From ab6621e01729e681d991ed756a415f8343a21e85 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 29 Jun 2023 10:24:13 -0500 Subject: [PATCH 001/237] Ensure rebase recovers repos in a broken state (#1247) rebase recovers from repos in a broken state --- packages/pds/src/services/repo/index.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/pds/src/services/repo/index.ts b/packages/pds/src/services/repo/index.ts index a6e2a010c45..4736f7caaea 100644 --- a/packages/pds/src/services/repo/index.ts +++ b/packages/pds/src/services/repo/index.ts @@ -2,8 +2,6 @@ import { CID } from 'multiformats/cid' import * as crypto from '@atproto/crypto' import { BlobStore, - MemoryBlockstore, - BlockMap, CommitData, RebaseData, Repo, @@ -272,25 +270,28 @@ export class RepoService { .where('did', '=', did) .select(['uri', 'cid']) .execute() - const memoryStore = new MemoryBlockstore() - let data = await repo.MST.create(memoryStore) + // this will do everything in memory & shouldn't touch storage until we do .getUnstoredBlocks + let data = await repo.MST.create(storage) for (const record of records) { const uri = new AtUri(record.uri) const cid = CID.parse(record.cid) const dataKey = repo.formatDataKey(uri.collection, uri.rkey) data = await data.add(dataKey, cid) } + // this looks for unstored blocks recursively & bails when it encounters a block it has + // in most cases, there should be no unstored blocks, but this allows for recovery of repos in a broken state + const unstoredData = await data.getUnstoredBlocks() const commit = await repo.signCommit( { did, version: 2, prev: null, - data: await data.getPointer(), + data: unstoredData.root, }, this.repoSigningKey, ) + const newBlocks = unstoredData.blocks const currCids = await data.allCids() - const newBlocks = new BlockMap() const commitCid = await newBlocks.add(commit) return { commit: commitCid, From 9263ddc64d0855d139d80e49a4c9772a9cc9d9cc Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 29 Jun 2023 10:24:48 -0500 Subject: [PATCH 002/237] Add compression in http res (#1022) * add compression * compress carfiles * update package version * add tests * one more test * remove compressible dep * add gzip to appview * fix package.json --- packages/bsky/package.json | 1 + packages/bsky/src/index.ts | 2 ++ packages/bsky/tests/server.test.ts | 30 +++++++++++++++++++ packages/pds/package.json | 1 + packages/pds/src/index.ts | 4 ++- packages/pds/src/util/compression.ts | 16 ++++++++++ packages/pds/tests/server.test.ts | 43 ++++++++++++++++++++++----- yarn.lock | 44 +++++++++++++++++++++++----- 8 files changed, 125 insertions(+), 16 deletions(-) create mode 100644 packages/pds/src/util/compression.ts diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 3215cb91990..c2144154d57 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -40,6 +40,7 @@ "@atproto/uri": "*", "@atproto/xrpc-server": "*", "@did-plc/lib": "^0.0.1", + "compression": "^1.7.4", "cors": "^2.8.5", "dotenv": "^16.0.0", "express": "^4.17.2", diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 3805f9a3376..708f9e463d4 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -4,6 +4,7 @@ import { AddressInfo } from 'net' import events from 'events' import { createHttpTerminator, HttpTerminator } from 'http-terminator' import cors from 'cors' +import compression from 'compression' import { IdResolver } from '@atproto/identity' import API, { health, blobResolver } from './api' import Database from './db' @@ -62,6 +63,7 @@ export class BskyAppView { const app = express() app.use(cors()) app.use(loggerMiddleware) + app.use(compression()) const didCache = new DidSqlCache( db, diff --git a/packages/bsky/tests/server.test.ts b/packages/bsky/tests/server.test.ts index 64b89491f82..e70660b7641 100644 --- a/packages/bsky/tests/server.test.ts +++ b/packages/bsky/tests/server.test.ts @@ -4,15 +4,23 @@ import axios, { AxiosError } from 'axios' import { TestNetwork } from '@atproto/dev-env' import { handler as errorHandler } from '../src/error' import { Database } from '../src' +import { SeedClient } from './seeds/client' +import basicSeed from './seeds/basic' describe('server', () => { let network: TestNetwork let db: Database + let alice: string beforeAll(async () => { network = await TestNetwork.create({ dbPostgresSchema: 'bsky_server', }) + const pdsAgent = network.pds.getClient() + const sc = new SeedClient(pdsAgent) + await basicSeed(sc) + await network.processAll() + alice = sc.dids.alice db = network.bsky.ctx.db }) @@ -80,6 +88,28 @@ describe('server', () => { }) }) + it('compresses large json responses', async () => { + const res = await axios.get( + `${network.bsky.url}/xrpc/app.bsky.feed.getTimeline`, + { + decompress: false, + headers: { + ...(await network.serviceHeaders(alice)), + 'accept-encoding': 'gzip', + }, + }, + ) + expect(res.headers['content-encoding']).toEqual('gzip') + }) + + it('does not compress small payloads', async () => { + const res = await axios.get(`${network.bsky.url}/xrpc/_health`, { + decompress: false, + headers: { 'accept-encoding': 'gzip' }, + }) + expect(res.headers['content-encoding']).toBeUndefined() + }) + it('healthcheck fails when database is unavailable.', async () => { await network.bsky.sub.destroy() await db.close() diff --git a/packages/pds/package.json b/packages/pds/package.json index df69f8e9a16..5c5f46ac407 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -43,6 +43,7 @@ "@did-plc/lib": "^0.0.1", "better-sqlite3": "^7.6.2", "bytes": "^3.1.2", + "compression": "^1.7.4", "cors": "^2.8.5", "dotenv": "^16.0.0", "express": "^4.17.2", diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 67f337f2f41..3f61323da8f 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -11,6 +11,7 @@ import events from 'events' import { createTransport } from 'nodemailer' import * as crypto from '@atproto/crypto' import { BlobStore } from '@atproto/repo' +import { IdResolver } from '@atproto/identity' import * as appviewConsumers from './app-view/event-stream/consumers' import inProcessAppView from './app-view/api' import API from './api' @@ -19,6 +20,7 @@ import * as wellKnown from './well-known' import Database from './db' import { ServerAuth } from './auth' import * as error from './error' +import compression from './util/compression' import { dbLogger, loggerMiddleware } from './logger' import { ServerConfig } from './config' import { ServerMailer } from './mailer' @@ -37,7 +39,6 @@ import { import { Labeler, HiveLabeler, KeywordLabeler } from './labeler' import { BackgroundQueue } from './event-stream/background-queue' import DidSqlCache from './did-cache' -import { IdResolver } from '@atproto/identity' import { MountedAlgos } from './feed-gen/types' import { Crawlers } from './crawlers' @@ -113,6 +114,7 @@ export class PDS { const app = express() app.use(cors()) app.use(loggerMiddleware) + app.use(compression()) let imgUriEndpoint = config.imgUriEndpoint if (!imgUriEndpoint) { diff --git a/packages/pds/src/util/compression.ts b/packages/pds/src/util/compression.ts new file mode 100644 index 00000000000..f79a672f2f5 --- /dev/null +++ b/packages/pds/src/util/compression.ts @@ -0,0 +1,16 @@ +import express from 'express' +import compression from 'compression' + +export default function () { + return compression({ + filter, + }) +} + +function filter(_req: express.Request, res: express.Response) { + const contentType = res.getHeader('Content-type') + if (contentType === 'application/vnd.ipld.car') { + return true + } + return compression.filter(_req, res) +} diff --git a/packages/pds/tests/server.test.ts b/packages/pds/tests/server.test.ts index 5912af3a0aa..ae9f34fed26 100644 --- a/packages/pds/tests/server.test.ts +++ b/packages/pds/tests/server.test.ts @@ -5,7 +5,7 @@ import AtpAgent from '@atproto/api' import { CloseFn, runTestServer, TestServerInfo } from './_util' import { handler as errorHandler } from '../src/error' import { SeedClient } from './seeds/client' -import usersSeed from './seeds/users' +import basicSeed from './seeds/basic' import { Database } from '../src' describe('server', () => { @@ -24,7 +24,7 @@ describe('server', () => { db = server.ctx.db agent = new AtpAgent({ service: server.url }) sc = new SeedClient(agent) - await usersSeed(sc) + await basicSeed(sc) alice = sc.dids.alice }) @@ -60,12 +60,6 @@ describe('server', () => { } }) - it('healthcheck succeeds when database is available.', async () => { - const { data, status } = await axios.get(`${server.url}/xrpc/_health`) - expect(status).toEqual(200) - expect(data).toEqual({ version: '0.0.0' }) - }) - it('limits size of json input.', async () => { let error: AxiosError try { @@ -91,6 +85,39 @@ describe('server', () => { }) }) + it('compresses large json responses', async () => { + const res = await axios.get( + `${server.url}/xrpc/app.bsky.feed.getTimeline`, + { + decompress: false, + headers: { ...sc.getHeaders(alice), 'accept-encoding': 'gzip' }, + }, + ) + expect(res.headers['content-encoding']).toEqual('gzip') + }) + + it('compresses large car file responses', async () => { + const res = await axios.get( + `${server.url}/xrpc/com.atproto.sync.getRepo?did=${alice}`, + { decompress: false, headers: { 'accept-encoding': 'gzip' } }, + ) + expect(res.headers['content-encoding']).toEqual('gzip') + }) + + it('does not compress small payloads', async () => { + const res = await axios.get(`${server.url}/xrpc/_health`, { + decompress: false, + headers: { 'accept-encoding': 'gzip' }, + }) + expect(res.headers['content-encoding']).toBeUndefined() + }) + + it('healthcheck succeeds when database is available.', async () => { + const { data, status } = await axios.get(`${server.url}/xrpc/_health`) + expect(status).toEqual(200) + expect(data).toEqual({ version: '0.0.0' }) + }) + it('healthcheck fails when database is unavailable.', async () => { // destroy to release lock & allow db to close await server.ctx.sequencerLeader.destroy() diff --git a/yarn.lock b/yarn.lock index d8c71fc3c43..957fe4fd0cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5029,7 +5029,7 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" -accepts@~1.3.8: +accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -5600,6 +5600,11 @@ byte-size@^7.0.0: resolved "https://registry.npmjs.org/byte-size/-/byte-size-7.0.1.tgz" integrity sha512-crQdqyCwhokxwV1UyDzLZanhkugAgft7vt0qbbdt60C6Zf3CAiGmtUCylbtYwrU6loOUw3euGrNtW1J651ot1A== +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + bytes@3.1.2, bytes@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -5928,6 +5933,26 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" @@ -9034,7 +9059,7 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" -mime-db@1.52.0: +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== @@ -9630,6 +9655,11 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" @@ -10734,16 +10764,16 @@ rxjs@^7.5.2: dependencies: tslib "^2.1.0" +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - safe-regex-test@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" From 7cb8c62b545db8c6139ec83939ff6ac236eca97a Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 29 Jun 2023 13:42:10 -0500 Subject: [PATCH 003/237] Repo update advisory lock (#1230) * use for no key update * use a tx advisory lock for repo updates * skip tests for sqlite * move check of commit swap on rebase * do lock before formatting rebase * hash schema in for lock id * no tx lock in sqlite * move rebase formatting to tx * move dialect check * rm log * make the lock ids a bit safer * change how we do lock id * refactor id generator --- packages/crypto/src/random.ts | 13 +++++++++ packages/dev-env/src/pds.ts | 4 ++- packages/pds/src/config.ts | 10 +++++++ packages/pds/src/db/index.ts | 30 +++++++++++++++++++-- packages/pds/src/services/repo/index.ts | 25 +++++++++--------- packages/pds/src/sql-repo-storage.ts | 14 +++------- packages/pds/tests/_util.ts | 3 +++ packages/pds/tests/db.test.ts | 35 ++++++++++++++++++++++++- packages/pds/tests/races.test.ts | 4 +-- 9 files changed, 109 insertions(+), 29 deletions(-) diff --git a/packages/crypto/src/random.ts b/packages/crypto/src/random.ts index e81682fb81d..a5c9bb460eb 100644 --- a/packages/crypto/src/random.ts +++ b/packages/crypto/src/random.ts @@ -1,6 +1,7 @@ import * as noble from '@noble/hashes/utils' import * as uint8arrays from 'uint8arrays' import { SupportedEncodings } from 'uint8arrays/to-string' +import { sha256 } from './sha' export const randomBytes = noble.randomBytes @@ -11,3 +12,15 @@ export const randomStr = ( const bytes = randomBytes(byteLength) return uint8arrays.toString(bytes, encoding) } + +export const randomIntFromSeed = async ( + seed: string, + high: number, + low = 0, +): Promise => { + const hash = await sha256(seed) + const number = Buffer.from(hash).readUintBE(0, 6) + const range = high - low + const normalized = number % range + return normalized + low +} diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index a7543583171..facb5ceed4b 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -1,7 +1,7 @@ import getPort from 'get-port' import * as ui8 from 'uint8arrays' import * as pds from '@atproto/pds' -import { Secp256k1Keypair } from '@atproto/crypto' +import { Secp256k1Keypair, randomStr } from '@atproto/crypto' import { MessageDispatcher } from '@atproto/pds/src/event-stream/message-queue' import { AtpAgent } from '@atproto/api' import { Client as PlcClient } from '@did-plc/lib' @@ -63,6 +63,7 @@ export class TestPds { labelerDid: 'did:example:labeler', labelerKeywords: { label_me: 'test-label', label_me_2: 'test-label-2' }, feedGenDid: 'did:example:feedGen', + dbTxLockNonce: await randomStr(32, 'base32'), ...cfg, }) @@ -71,6 +72,7 @@ export class TestPds { ? pds.Database.postgres({ url: config.dbPostgresUrl, schema: config.dbPostgresSchema, + txLockNonce: config.dbTxLockNonce, }) : pds.Database.memory() await db.migrateToLatestOrThrow() diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index cd4ce6b7dd4..c884721b1e5 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -55,6 +55,9 @@ export interface ServerConfigValues { repoBackfillLimitMs: number sequencerLeaderLockId?: number + // this is really only used in test environments + dbTxLockNonce?: string + bskyAppViewEndpoint?: string bskyAppViewDid?: string @@ -172,6 +175,8 @@ export class ServerConfig { undefined, ) + const dbTxLockNonce = nonemptyString(process.env.DB_TX_LOCK_NONCE) + const bskyAppViewEndpoint = nonemptyString( process.env.BSKY_APP_VIEW_ENDPOINT, ) @@ -221,6 +226,7 @@ export class ServerConfig { maxSubscriptionBuffer, repoBackfillLimitMs, sequencerLeaderLockId, + dbTxLockNonce, bskyAppViewEndpoint, bskyAppViewDid, crawlersToNotify, @@ -414,6 +420,10 @@ export class ServerConfig { return this.cfg.sequencerLeaderLockId } + get dbTxLockNonce() { + return this.cfg.dbTxLockNonce + } + get bskyAppViewEndpoint() { return this.cfg.bskyAppViewEndpoint } diff --git a/packages/pds/src/db/index.ts b/packages/pds/src/db/index.ts index 6cf0486dca7..52941047a2c 100644 --- a/packages/pds/src/db/index.ts +++ b/packages/pds/src/db/index.ts @@ -22,10 +22,12 @@ import { dummyDialect } from './util' import * as migrations from './migrations' import { CtxMigrationProvider } from './migrations/provider' import { dbLogger as log } from '../logger' +import { randomIntFromSeed } from '@atproto/crypto' export class Database { txEvt = new EventEmitter() as TxnEmitter txChannelEvts: ChannelEvt[] = [] + txLockNonce: string | undefined channels: Channels migrator: Migrator destroyed = false @@ -46,6 +48,7 @@ export class Database { new_repo_event: new EventEmitter() as ChannelEmitter, outgoing_repo_seq: new EventEmitter() as ChannelEmitter, } + this.txLockNonce = cfg.dialect === 'pg' ? cfg.txLockNonce : undefined } static sqlite(location: string): Database { @@ -58,7 +61,7 @@ export class Database { } static postgres(opts: PgOptions): Database { - const { schema, url } = opts + const { schema, url, txLockNonce } = opts const pool = opts.pool ?? new PgPool({ @@ -89,7 +92,13 @@ export class Database { dialect: new PostgresDialect({ pool }), }) - return new Database(db, { dialect: 'pg', pool, schema, url }) + return new Database(db, { + dialect: 'pg', + pool, + schema, + url, + txLockNonce, + }) } static memory(): Database { @@ -182,6 +191,17 @@ export class Database { return txRes } + async txAdvisoryLock(name: string): Promise { + this.assertTransaction() + assert(this.dialect === 'pg', 'Postgres required') + // any lock id < 10k is reserved for session locks + const id = await randomIntFromSeed(name, Number.MAX_SAFE_INTEGER, 10000) + const res = (await sql`SELECT pg_try_advisory_xact_lock(${sql.literal( + id, + )}) as acquired`.execute(this.db)) as TxLockRes + return res.rows[0]?.acquired === true + } + get schema(): string | undefined { return this.cfg.dialect === 'pg' ? this.cfg.schema : undefined } @@ -299,6 +319,7 @@ export type PgConfig = { pool: PgPool url: string schema?: string + txLockNonce?: string } export type SqliteConfig = { @@ -315,6 +336,7 @@ type PgOptions = { poolSize?: number poolMaxUses?: number poolIdleTimeoutMs?: number + txLockNonce?: string } type ChannelEvents = { @@ -356,3 +378,7 @@ class LeakyTxPlugin implements KyselyPlugin { return args.result } } + +type TxLockRes = { + rows: { acquired: true | false }[] +} diff --git a/packages/pds/src/services/repo/index.ts b/packages/pds/src/services/repo/index.ts index 4736f7caaea..c41e01b4c18 100644 --- a/packages/pds/src/services/repo/index.ts +++ b/packages/pds/src/services/repo/index.ts @@ -112,8 +112,8 @@ export class RepoService { ) { this.db.assertTransaction() const storage = new SqlRepoStorage(this.db, did, now) - const locked = await storage.lockHead() - if (!locked || !locked.equals(commitData.prev)) { + const locked = await storage.lockRepo() + if (!locked) { throw new ConcurrentWriteError() } await Promise.all([ @@ -245,23 +245,27 @@ export class RepoService { async rebaseRepo(did: string, swapCommit?: CID) { this.db.assertNotTransaction() - const rebaseData = await this.formatRebase(did, swapCommit) // rebases are expensive & should be done rarely, we don't try to re-process on concurrent writes - await this.serviceTx(async (srvcTx) => - srvcTx.processRebase(did, rebaseData), - ) + await this.serviceTx(async (srvcTx) => { + const rebaseData = await srvcTx.formatRebase(did, swapCommit) + await srvcTx.processRebase(did, rebaseData) + }) } async formatRebase(did: string, swapCommit?: CID): Promise { const storage = new SqlRepoStorage(this.db, did, new Date().toISOString()) + const locked = await storage.lockRepo() + if (!locked) { + throw new ConcurrentWriteError() + } + const currRoot = await storage.getHead() if (!currRoot) { throw new InvalidRequestError( `${did} is not a registered repo on this server`, ) - } - if (swapCommit && !currRoot.equals(swapCommit)) { + } else if (swapCommit && !currRoot.equals(swapCommit)) { throw new BadCommitSwapError(currRoot) } @@ -303,11 +307,8 @@ export class RepoService { async processRebase(did: string, rebaseData: RebaseData) { this.db.assertTransaction() + const storage = new SqlRepoStorage(this.db, did) - const lockedHead = await storage.lockHead() - if (!rebaseData.rebased.equals(lockedHead)) { - throw new ConcurrentWriteError() - } const recordCountBefore = await this.countRecordBlocks(did) await Promise.all([ diff --git a/packages/pds/src/sql-repo-storage.ts b/packages/pds/src/sql-repo-storage.ts index b3ce8a8cb05..720c09b50ca 100644 --- a/packages/pds/src/sql-repo-storage.ts +++ b/packages/pds/src/sql-repo-storage.ts @@ -26,17 +26,9 @@ export class SqlRepoStorage extends RepoStorage { } // note this method will return null if the repo has a lock on it currently - async lockHead(): Promise { - let builder = this.db.db - .selectFrom('repo_root') - .selectAll() - .where('did', '=', this.did) - if (this.db.dialect !== 'sqlite') { - builder = builder.forUpdate().skipLocked() - } - const res = await builder.executeTakeFirst() - if (!res) return null - return CID.parse(res.root) + async lockRepo(): Promise { + if (this.db.dialect === 'sqlite') return true + return this.db.txAdvisoryLock(this.did) } async getHead(): Promise { diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index b6c7d5cdee7..7d33a7a48b1 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -105,6 +105,7 @@ export const runTestServer = async ( maxSubscriptionBuffer: 200, repoBackfillLimitMs: HOUR, sequencerLeaderLockId: uniqueLockId(), + dbTxLockNonce: await randomStr(32, 'base32'), ...params, }) @@ -113,6 +114,7 @@ export const runTestServer = async ( ? Database.postgres({ url: cfg.dbPostgresUrl, schema: cfg.dbPostgresSchema, + txLockNonce: cfg.dbTxLockNonce, }) : Database.memory() @@ -123,6 +125,7 @@ export const runTestServer = async ( ? Database.postgres({ url: cfg.dbPostgresUrl, schema: cfg.dbPostgresSchema, + txLockNonce: cfg.dbTxLockNonce, }) : db if (opts.migration) { diff --git a/packages/pds/tests/db.test.ts b/packages/pds/tests/db.test.ts index eb200f4d489..5ebbc788eeb 100644 --- a/packages/pds/tests/db.test.ts +++ b/packages/pds/tests/db.test.ts @@ -1,6 +1,6 @@ import { sql } from 'kysely' import { once } from 'events' -import { wait } from '@atproto/common' +import { createDeferrable, wait } from '@atproto/common' import { Database } from '../src' import { Leader, appMigration } from '../src/db/leader' import { runTestServer, CloseFn } from './_util' @@ -168,6 +168,39 @@ describe('db', () => { }) }) + describe('transaction advisory locks', () => { + it('allows locks in txs to run sequentially', async () => { + if (db.dialect !== 'pg') return + for (let i = 0; i < 100; i++) { + await db.transaction(async (dbTxn) => { + const locked = await dbTxn.txAdvisoryLock('asfd') + expect(locked).toBe(true) + }) + } + }) + + it('locks block between txns', async () => { + if (db.dialect !== 'pg') return + const deferable = createDeferrable() + const tx1 = db.transaction(async (dbTxn) => { + const locked = await dbTxn.txAdvisoryLock('asdf') + expect(locked).toBe(true) + await deferable.complete + }) + // give it just a second to ensure it gets the lock + await wait(10) + const tx2 = db.transaction(async (dbTxn) => { + const locked = await dbTxn.txAdvisoryLock('asdf') + expect(locked).toBe(false) + deferable.resolve() + await tx1 + const locked2 = await dbTxn.txAdvisoryLock('asdf') + expect(locked2).toBe(true) + }) + await tx2 + }) + }) + describe('Leader', () => { it('allows leaders to run sequentially.', async () => { const task = async () => { diff --git a/packages/pds/tests/races.test.ts b/packages/pds/tests/races.test.ts index 90680ad5ec6..b30ac561621 100644 --- a/packages/pds/tests/races.test.ts +++ b/packages/pds/tests/races.test.ts @@ -59,8 +59,8 @@ describe('crud operations', () => { const now = new Date().toISOString() await ctx.db.transaction(async (dbTxn) => { const storage = new SqlRepoStorage(dbTxn, did, now) - const locked = await storage.lockHead() - if (!locked || !locked.equals(commitData.prev)) { + const locked = await storage.lockRepo() + if (!locked) { throw new ConcurrentWriteError() } await wait(waitMs) From e81ff25f02e1beaca58389b138a618f8727bb322 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 29 Jun 2023 15:44:19 -0500 Subject: [PATCH 004/237] Throw concurrent write error on prev mismatch (#1254) * throw concurrent write error * build branch * remove branch build --- packages/pds/src/sql-repo-storage.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/pds/src/sql-repo-storage.ts b/packages/pds/src/sql-repo-storage.ts index 720c09b50ca..3002ebe6ecf 100644 --- a/packages/pds/src/sql-repo-storage.ts +++ b/packages/pds/src/sql-repo-storage.ts @@ -13,6 +13,7 @@ import { valuesList } from './db/util' import { IpldBlock } from './db/tables/ipld-block' import { RepoCommitBlock } from './db/tables/repo-commit-block' import { RepoCommitHistory } from './db/tables/repo-commit-history' +import { ConcurrentWriteError } from './services/repo' export class SqlRepoStorage extends RepoStorage { cache: BlockMap = new BlockMap() @@ -236,7 +237,7 @@ export class SqlRepoStorage extends RepoStorage { .where('root', '=', prev.toString()) .executeTakeFirst() if (res.numUpdatedRows < 1) { - throw new Error('failed to update repo root: misordered') + throw new ConcurrentWriteError() } } } From 8f8f2c4b73acd24da86a8f2acc99f5bb3d1d95ab Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 29 Jun 2023 19:33:04 -0500 Subject: [PATCH 005/237] Prevent duplicate blocks in checkout (#1256) * prevent duplicate cids in checkout * tidy --- packages/repo/src/mst/mst.ts | 8 -------- packages/repo/tests/sync/checkout.test.ts | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/repo/src/mst/mst.ts b/packages/repo/src/mst/mst.ts index ad918644121..e3255219b0e 100644 --- a/packages/repo/src/mst/mst.ts +++ b/packages/repo/src/mst/mst.ts @@ -694,17 +694,9 @@ export class MST { // Sync Protocol async writeToCarStream(car: BlockWriter): Promise { - const entries = await this.getEntries() const leaves = new CidSet() let toFetch = new CidSet() toFetch.add(await this.getPointer()) - for (const entry of entries) { - if (entry.isLeaf()) { - leaves.add(entry.value) - } else { - toFetch.add(await entry.getPointer()) - } - } while (toFetch.size() > 0) { const nextLayer = new CidSet() const fetched = await this.storage.getBlocks(toFetch.toList()) diff --git a/packages/repo/tests/sync/checkout.test.ts b/packages/repo/tests/sync/checkout.test.ts index 1c213087268..df0edec2a70 100644 --- a/packages/repo/tests/sync/checkout.test.ts +++ b/packages/repo/tests/sync/checkout.test.ts @@ -1,10 +1,11 @@ import * as crypto from '@atproto/crypto' -import { Repo, RepoContents, RepoVerificationError } from '../../src' +import { CidSet, Repo, RepoContents, RepoVerificationError } from '../../src' import { MemoryBlockstore } from '../../src/storage' import * as sync from '../../src/sync' import * as util from '../_util' import { streamToBuffer } from '@atproto/common' +import { CarReader } from '@ipld/car/reader' describe('Checkout Sync', () => { let storage: MemoryBlockstore @@ -50,6 +51,18 @@ describe('Checkout Sync', () => { expect(hasGenesisCommit).toBeFalsy() }) + it('does not sync duplicate blocks', async () => { + const carBytes = await streamToBuffer(sync.getCheckout(storage, repo.cid)) + const car = await CarReader.fromBytes(carBytes) + const cids = new CidSet() + for await (const block of car.blocks()) { + if (cids.has(block.cid)) { + throw new Error(`duplicate block: :${block.cid.toString()}`) + } + cids.add(block.cid) + } + }) + it('throws on a bad signature', async () => { const badRepo = await util.addBadCommit(repo, keypair) const checkoutCar = await streamToBuffer( From ee68a4037bb30eb7a7169d57a29ce6ad1d2254b6 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Fri, 30 Jun 2023 16:02:47 -0500 Subject: [PATCH 006/237] Appview - Invalid handles (#1244) * run with-test-db in monorepo test * improve pg script * tidy * namespace bsky pg schemas * differentiate schemas * clean up script * first pass at invalid handles in appview * tests for handle invalidation * move mock to instance instead of prototype * change network mocks in general * fixing pagination on actor-search * fix snap & normalize handles on index * handling did pagination + update tests * one last update --- .../src/api/com/atproto/admin/searchRepos.ts | 2 +- .../20230627T212437895Z-optional-handle.ts | 17 ++ packages/bsky/src/db/migrations/index.ts | 1 + packages/bsky/src/db/tables/actor.ts | 2 +- packages/bsky/src/services/actor/index.ts | 10 +- packages/bsky/src/services/actor/views.ts | 5 +- packages/bsky/src/services/feed/index.ts | 3 +- packages/bsky/src/services/indexing/index.ts | 40 ++-- .../bsky/src/services/moderation/views.ts | 3 +- packages/bsky/src/services/util/search.ts | 14 +- .../bsky/tests/handle-invalidation.test.ts | 129 +++++++++++++ .../__snapshots__/actor-search.test.ts.snap | 76 ++++---- .../bsky/tests/views/actor-search.test.ts | 18 +- packages/dev-env/src/network.ts | 2 +- packages/dev-env/src/util.ts | 25 ++- packages/identifier/src/handle.ts | 2 + packages/pds/tests/_util.ts | 5 + .../proxied/__snapshots__/views.test.ts.snap | 177 +++++++++--------- packages/pds/tests/proxied/views.test.ts | 16 +- packages/pds/tests/views/actor-search.test.ts | 82 ++++---- 20 files changed, 420 insertions(+), 209 deletions(-) create mode 100644 packages/bsky/src/db/migrations/20230627T212437895Z-optional-handle.ts create mode 100644 packages/bsky/tests/handle-invalidation.test.ts diff --git a/packages/bsky/src/api/com/atproto/admin/searchRepos.ts b/packages/bsky/src/api/com/atproto/admin/searchRepos.ts index 2e7bbcd3f47..c81218ba93f 100644 --- a/packages/bsky/src/api/com/atproto/admin/searchRepos.ts +++ b/packages/bsky/src/api/com/atproto/admin/searchRepos.ts @@ -19,7 +19,7 @@ export default function (server: Server, ctx: AppContext) { const searchField = term.startsWith('did:') ? 'did' : 'handle' const { ref } = db.db.dynamic - const keyset = new ListKeyset(ref('indexedAt'), ref('handle')) + const keyset = new ListKeyset(ref('indexedAt'), ref('did')) let resultQb = services.actor(db).searchQb(searchField, term).selectAll() resultQb = paginate(resultQb, { keyset, cursor, limit }) diff --git a/packages/bsky/src/db/migrations/20230627T212437895Z-optional-handle.ts b/packages/bsky/src/db/migrations/20230627T212437895Z-optional-handle.ts new file mode 100644 index 00000000000..74279ef12e1 --- /dev/null +++ b/packages/bsky/src/db/migrations/20230627T212437895Z-optional-handle.ts @@ -0,0 +1,17 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .alterTable('actor') + .alterColumn('handle') + .dropNotNull() + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema + .alterTable('actor') + .alterColumn('handle') + .setNotNull() + .execute() +} diff --git a/packages/bsky/src/db/migrations/index.ts b/packages/bsky/src/db/migrations/index.ts index e575cd70103..1bbc9cf8f34 100644 --- a/packages/bsky/src/db/migrations/index.ts +++ b/packages/bsky/src/db/migrations/index.ts @@ -17,3 +17,4 @@ export * as _20230609T232122649Z from './20230609T232122649Z-actor-deletion-inde export * as _20230610T203555962Z from './20230610T203555962Z-suggested-follows' export * as _20230611T215300060Z from './20230611T215300060Z-actor-state' export * as _20230620T161134972Z from './20230620T161134972Z-post-langs' +export * as _20230627T212437895Z from './20230627T212437895Z-optional-handle' diff --git a/packages/bsky/src/db/tables/actor.ts b/packages/bsky/src/db/tables/actor.ts index 5a7c20f9f39..312c5808cab 100644 --- a/packages/bsky/src/db/tables/actor.ts +++ b/packages/bsky/src/db/tables/actor.ts @@ -1,6 +1,6 @@ export interface Actor { did: string - handle: string + handle: string | null indexedAt: string takedownId: number | null // @TODO(bsky) } diff --git a/packages/bsky/src/services/actor/index.ts b/packages/bsky/src/services/actor/index.ts index be3fbf669e5..3d053242bad 100644 --- a/packages/bsky/src/services/actor/index.ts +++ b/packages/bsky/src/services/actor/index.ts @@ -66,8 +66,8 @@ export class ActorService { .execute() return results.sort((a, b) => { - const orderA = order[a.did] ?? order[a.handle.toLowerCase()] - const orderB = order[b.did] ?? order[b.handle.toLowerCase()] + const orderA = order[a.did] ?? order[a.handle?.toLowerCase() ?? ''] + const orderB = order[b.did] ?? order[b.handle?.toLowerCase() ?? ''] return orderA - orderB }) } @@ -110,9 +110,9 @@ const distance = (term: string, ref: DbRef) => export class ListKeyset extends TimeCidKeyset<{ indexedAt: string - handle: string // handles are treated identically to cids in TimeCidKeyset + did: string // handles are treated identically to cids in TimeCidKeyset }> { - labelResult(result: { indexedAt: string; handle: string }) { - return { primary: result.indexedAt, secondary: result.handle } + labelResult(result: { indexedAt: string; did: string }) { + return { primary: result.indexedAt, secondary: result.did } } } diff --git a/packages/bsky/src/services/actor/views.ts b/packages/bsky/src/services/actor/views.ts index 346406c74c5..ecdc43627a5 100644 --- a/packages/bsky/src/services/actor/views.ts +++ b/packages/bsky/src/services/actor/views.ts @@ -1,4 +1,5 @@ import { ArrayEl } from '@atproto/common' +import { INVALID_HANDLE } from '@atproto/identifier' import { ProfileViewDetailed, ProfileView, @@ -117,7 +118,7 @@ export class ActorViews { : undefined return { did: result.did, - handle: result.handle, + handle: result.handle ?? INVALID_HANDLE, displayName: profileInfo?.displayName || undefined, description: profileInfo?.description || undefined, avatar, @@ -224,7 +225,7 @@ export class ActorViews { : undefined return { did: result.did, - handle: result.handle, + handle: result.handle ?? INVALID_HANDLE, displayName: profileInfo?.displayName || undefined, description: profileInfo?.description || undefined, avatar, diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index 43e747c903b..d9beb7c7871 100644 --- a/packages/bsky/src/services/feed/index.ts +++ b/packages/bsky/src/services/feed/index.ts @@ -1,6 +1,7 @@ import { sql } from 'kysely' import { AtUri } from '@atproto/uri' import { dedupeStrs } from '@atproto/common' +import { INVALID_HANDLE } from '@atproto/identifier' import Database from '../../db' import { countAll, noMatch, notSoftDeletedClause } from '../../db/util' import { ImageUriBuilder } from '../../image/uri' @@ -166,7 +167,7 @@ export class FeedService { ...acc, [cur.did]: { did: cur.did, - handle: cur.handle, + handle: cur.handle ?? INVALID_HANDLE, displayName: cur.displayName ?? undefined, avatar: cur.avatarCid ? this.imgUriBuilder.getCommonSignedUri( diff --git a/packages/bsky/src/services/indexing/index.ts b/packages/bsky/src/services/indexing/index.ts index 02265e12446..da79cbb6345 100644 --- a/packages/bsky/src/services/indexing/index.ts +++ b/packages/bsky/src/services/indexing/index.ts @@ -127,25 +127,39 @@ export class IndexingService { if (!needsReindex) { return } - const { handle } = await this.idResolver.did.resolveAtprotoData(did, true) - const handleToDid = await this.idResolver.handle.resolve(handle) - if (did !== handleToDid) { - return // No bidirectional link between did and handle + const atpData = await this.idResolver.did.resolveAtprotoData(did, true) + const handleToDid = await this.idResolver.handle.resolve(atpData.handle) + + const handle: string | null = + did === handleToDid ? atpData.handle.toLowerCase() : null + + if (actor && actor.handle !== handle) { + const actorWithHandle = + handle !== null + ? await this.db.db + .selectFrom('actor') + .where('handle', '=', handle) + .selectAll() + .executeTakeFirst() + : null + + // handle contention + if (handle && actorWithHandle && did !== actorWithHandle.did) { + await this.db.db + .updateTable('actor') + .where('actor.did', '=', actorWithHandle.did) + .set({ handle: null }) + .execute() + } } + const actorInfo = { handle, indexedAt: timestamp } - const inserted = await this.db.db + await this.db.db .insertInto('actor') .values({ did, ...actorInfo }) - .onConflict((oc) => oc.doNothing()) + .onConflict((oc) => oc.column('did').doUpdateSet(actorInfo)) .returning('did') .executeTakeFirst() - if (!inserted) { - await this.db.db - .updateTable('actor') - .set(actorInfo) - .where('did', '=', did) - .execute() - } } async indexRepo(did: string, commit: string) { diff --git a/packages/bsky/src/services/moderation/views.ts b/packages/bsky/src/services/moderation/views.ts index a79c81749d9..ac5130b5283 100644 --- a/packages/bsky/src/services/moderation/views.ts +++ b/packages/bsky/src/services/moderation/views.ts @@ -1,6 +1,7 @@ import { Selectable } from 'kysely' import { ArrayEl } from '@atproto/common' import { AtUri } from '@atproto/uri' +import { INVALID_HANDLE } from '@atproto/identifier' import { BlobRef, jsonStringToLex } from '@atproto/lexicon' import Database from '../../db' import { Actor } from '../../db/tables/actor' @@ -82,7 +83,7 @@ export class ModerationViews { return { // No email or invite info on appview did: r.did, - handle: r.handle, + handle: r.handle ?? INVALID_HANDLE, relatedRecords, indexedAt: r.indexedAt, moderation: { diff --git a/packages/bsky/src/services/util/search.ts b/packages/bsky/src/services/util/search.ts index 826854941f7..f32ce468f12 100644 --- a/packages/bsky/src/services/util/search.ts +++ b/packages/bsky/src/services/util/search.ts @@ -22,7 +22,7 @@ export const getUserSearchQuery = ( limit, cursor, direction: 'asc', - keyset: new SearchKeyset(distanceAccount, ref('handle')), + keyset: new SearchKeyset(distanceAccount, ref('actor.did')), }) // Matching profiles based on display name const distanceProfile = distance(term, ref('displayName')) @@ -31,14 +31,14 @@ export const getUserSearchQuery = ( limit, cursor, direction: 'asc', - keyset: new SearchKeyset(distanceProfile, ref('handle')), + keyset: new SearchKeyset(distanceProfile, ref('actor.did')), }) // Combine and paginate result set return paginate(combineAccountsAndProfilesQb(db, accountsQb, profilesQb), { limit, cursor, direction: 'asc', - keyset: new SearchKeyset(ref('distance'), ref('handle')), + keyset: new SearchKeyset(ref('distance'), ref('actor.did')), }) } @@ -64,7 +64,7 @@ export const getUserSearchQuerySimple = ( return paginate(combineAccountsAndProfilesQb(db, accountsQb, profilesQb), { limit, direction: 'asc', - keyset: new SearchKeyset(ref('distance'), ref('handle')), + keyset: new SearchKeyset(ref('distance'), ref('actor.did')), }) } @@ -81,6 +81,7 @@ const getMatchingAccountsQb = ( .if(!includeSoftDeleted, (qb) => qb.where(notSoftDeletedClause(ref('actor'))), ) + .where('actor.handle', 'is not', null) .where(similar(term, ref('handle'))) // Coarse filter engaging trigram index .where(distanceAccount, '<', getMatchThreshold(term)) // Refines results from trigram index .select(['actor.did as did', distanceAccount.as('distance')]) @@ -100,6 +101,7 @@ const getMatchingProfilesQb = ( .if(!includeSoftDeleted, (qb) => qb.where(notSoftDeletedClause(ref('actor'))), ) + .where('actor.handle', 'is not', null) .where(similar(term, ref('displayName'))) // Coarse filter engaging trigram index .where(distanceProfile, '<', getMatchThreshold(term)) // Refines results from trigram index .select(['profile.creator as did', distanceProfile.as('distance')]) @@ -149,13 +151,13 @@ const getMatchThreshold = (term: string) => { return term.length < 3 ? 0.9 : 0.8 } -type Result = { distance: number; handle: string } +type Result = { distance: number; did: string } type LabeledResult = { primary: number; secondary: string } export class SearchKeyset extends GenericKeyset { labelResult(result: Result) { return { primary: result.distance, - secondary: result.handle, + secondary: result.did, } } labeledResultToCursor(labeled: LabeledResult) { diff --git a/packages/bsky/tests/handle-invalidation.test.ts b/packages/bsky/tests/handle-invalidation.test.ts new file mode 100644 index 00000000000..9918ee08826 --- /dev/null +++ b/packages/bsky/tests/handle-invalidation.test.ts @@ -0,0 +1,129 @@ +import { DAY } from '@atproto/common' +import { TestNetwork } from '@atproto/dev-env' +import { AtpAgent } from '@atproto/api' +import { SeedClient } from './seeds/client' +import userSeed from './seeds/users' + +describe('handle invalidation', () => { + let network: TestNetwork + let agent: AtpAgent + let pdsAgent: AtpAgent + let sc: SeedClient + let alice: string + let bob: string + + const mockHandles = {} + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_handle_invalidation', + }) + agent = network.bsky.getClient() + pdsAgent = network.pds.getClient() + sc = new SeedClient(pdsAgent) + await userSeed(sc) + await network.processAll() + + alice = sc.dids.alice + bob = sc.dids.bob + + const origResolve = network.bsky.ctx.idResolver.handle.resolve + network.bsky.ctx.idResolver.handle.resolve = async (handle: string) => { + if (mockHandles[handle] === null) { + return undefined + } else if (mockHandles[handle]) { + return mockHandles[handle] + } + return origResolve(handle) + } + }) + + afterAll(async () => { + await network.close() + }) + + const backdateIndexedAt = async (did: string) => { + const TWO_DAYS_AGO = new Date(Date.now() - 2 * DAY).toISOString() + await network.bsky.ctx.db.db + .updateTable('actor') + .set({ indexedAt: TWO_DAYS_AGO }) + .where('did', '=', did) + .execute() + } + + it('indexes an account with no proper handle', async () => { + mockHandles['eve.test'] = null + const eveAccnt = await sc.createAccount('eve', { + handle: 'eve.test', + email: 'eve@test.com', + password: 'eve-pass', + }) + await network.processAll() + + const res = await agent.api.app.bsky.actor.getProfile( + { actor: eveAccnt.did }, + { headers: await network.serviceHeaders(alice) }, + ) + expect(res.data.handle).toEqual('handle.invalid') + }) + + it('invalidates out of date handles', async () => { + await backdateIndexedAt(alice) + + const aliceHandle = sc.accounts[alice].handle + // alice's handle no longer resolves + mockHandles[aliceHandle] = null + await sc.post(alice, 'blah') + await network.processAll() + const res = await agent.api.app.bsky.actor.getProfile( + { actor: alice }, + { headers: await network.serviceHeaders(alice) }, + ) + expect(res.data.handle).toEqual('handle.invalid') + }) + + it('revalidates an out of date handle', async () => { + await backdateIndexedAt(alice) + const aliceHandle = sc.accounts[alice].handle + // alice's handle no longer resolves + delete mockHandles[aliceHandle] + + await sc.post(alice, 'blah') + await network.processAll() + const res = await agent.api.app.bsky.actor.getProfile( + { actor: alice }, + { headers: await network.serviceHeaders(alice) }, + ) + expect(res.data.handle).toEqual(sc.accounts[alice].handle) + }) + + it('deals with handle contention', async () => { + await backdateIndexedAt(bob) + // update alices handle so that the pds will let bob take her old handle + await network.pds.ctx.db.db + .updateTable('did_handle') + .where('did', '=', alice) + .set({ handle: 'not-alice.test' }) + .execute() + + await pdsAgent.api.com.atproto.identity.updateHandle( + { + handle: sc.accounts[alice].handle, + }, + { headers: sc.getHeaders(bob), encoding: 'application/json' }, + ) + await network.processAll() + + const aliceRes = await agent.api.app.bsky.actor.getProfile( + { actor: alice }, + { headers: await network.serviceHeaders(alice) }, + ) + expect(aliceRes.data.handle).toEqual('handle.invalid') + + const bobRes = await agent.api.app.bsky.actor.getProfile( + { actor: bob }, + { headers: await network.serviceHeaders(alice) }, + ) + expect(bobRes.data.handle).toEqual(sc.accounts[alice].handle) + }) +}) diff --git a/packages/bsky/tests/views/__snapshots__/actor-search.test.ts.snap b/packages/bsky/tests/views/__snapshots__/actor-search.test.ts.snap index f5fc00b818b..c0441c49b6d 100644 --- a/packages/bsky/tests/views/__snapshots__/actor-search.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/actor-search.test.ts.snap @@ -3,8 +3,11 @@ exports[`pds actor search views search gives relevant results 1`] = ` Array [ Object { + "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", "did": "user(0)", - "handle": "cara-wiegand69.test", + "displayName": "Carlton Abernathy IV", + "handle": "aliya-hodkiewicz.test", + "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, @@ -12,11 +15,8 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(0)@jpeg", - "did": "user(1)", - "displayName": "Carol Littel", - "handle": "eudora-dietrich4.test", - "indexedAt": "1970-01-01T00:00:00.000Z", + "did": "user(2)", + "handle": "cara-wiegand69.test", "labels": Array [], "viewer": Object { "blockedBy": false, @@ -24,11 +24,8 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(0)@jpeg", "did": "user(3)", - "displayName": "Sadie Carter", - "handle": "shane-torphy52.test", - "indexedAt": "1970-01-01T00:00:00.000Z", + "handle": "carlos6.test", "labels": Array [], "viewer": Object { "blockedBy": false, @@ -36,10 +33,10 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(0)@jpeg", - "did": "user(5)", - "displayName": "Carlton Abernathy IV", - "handle": "aliya-hodkiewicz.test", + "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(0)@jpeg", + "did": "user(4)", + "displayName": "Latoya Windler", + "handle": "carolina-mcdermott77.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { @@ -48,8 +45,11 @@ Array [ }, }, Object { - "did": "user(7)", - "handle": "carlos6.test", + "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(7)/cids(0)@jpeg", + "did": "user(6)", + "displayName": "Rachel Kshlerin", + "handle": "cayla-marquardt39.test", + "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, @@ -59,8 +59,8 @@ Array [ Object { "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(9)/cids(0)@jpeg", "did": "user(8)", - "displayName": "Latoya Windler", - "handle": "carolina-mcdermott77.test", + "displayName": "Carol Littel", + "handle": "eudora-dietrich4.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { @@ -71,8 +71,8 @@ Array [ Object { "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(11)/cids(0)@jpeg", "did": "user(10)", - "displayName": "Rachel Kshlerin", - "handle": "cayla-marquardt39.test", + "displayName": "Sadie Carter", + "handle": "shane-torphy52.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { @@ -86,46 +86,46 @@ Array [ exports[`pds actor search views typeahead gives relevant results 1`] = ` Array [ Object { + "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", "did": "user(0)", - "handle": "cara-wiegand69.test", + "displayName": "Carlton Abernathy IV", + "handle": "aliya-hodkiewicz.test", "viewer": Object { "blockedBy": false, "muted": false, }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(0)@jpeg", - "did": "user(1)", - "displayName": "Carol Littel", - "handle": "eudora-dietrich4.test", + "did": "user(2)", + "handle": "cara-wiegand69.test", "viewer": Object { "blockedBy": false, "muted": false, }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(0)@jpeg", "did": "user(3)", - "displayName": "Sadie Carter", - "handle": "shane-torphy52.test", + "handle": "carlos6.test", "viewer": Object { "blockedBy": false, "muted": false, }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(0)@jpeg", - "did": "user(5)", - "displayName": "Carlton Abernathy IV", - "handle": "aliya-hodkiewicz.test", + "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(0)@jpeg", + "did": "user(4)", + "displayName": "Latoya Windler", + "handle": "carolina-mcdermott77.test", "viewer": Object { "blockedBy": false, "muted": false, }, }, Object { - "did": "user(7)", - "handle": "carlos6.test", + "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(7)/cids(0)@jpeg", + "did": "user(6)", + "displayName": "Rachel Kshlerin", + "handle": "cayla-marquardt39.test", "viewer": Object { "blockedBy": false, "muted": false, @@ -134,8 +134,8 @@ Array [ Object { "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(9)/cids(0)@jpeg", "did": "user(8)", - "displayName": "Latoya Windler", - "handle": "carolina-mcdermott77.test", + "displayName": "Carol Littel", + "handle": "eudora-dietrich4.test", "viewer": Object { "blockedBy": false, "muted": false, @@ -144,8 +144,8 @@ Array [ Object { "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(11)/cids(0)@jpeg", "did": "user(10)", - "displayName": "Rachel Kshlerin", - "handle": "cayla-marquardt39.test", + "displayName": "Sadie Carter", + "handle": "shane-torphy52.test", "viewer": Object { "blockedBy": false, "muted": false, diff --git a/packages/bsky/tests/views/actor-search.test.ts b/packages/bsky/tests/views/actor-search.test.ts index 39b9ce18a1a..fa690f8c5cf 100644 --- a/packages/bsky/tests/views/actor-search.test.ts +++ b/packages/bsky/tests/views/actor-search.test.ts @@ -83,7 +83,10 @@ describe('pds actor search views', () => { shouldNotContain.forEach((handle) => expect(handles).not.toContain(handle)) - expect(forSnapshot(result.data.actors)).toMatchSnapshot() + const sorted = result.data.actors.sort((a, b) => + a.handle > b.handle ? 1 : -1, + ) + expect(forSnapshot(sorted)).toMatchSnapshot() }) it('typeahead gives empty result set when provided empty term', async () => { @@ -158,7 +161,10 @@ describe('pds actor search views', () => { shouldNotContain.forEach((handle) => expect(handles).not.toContain(handle)) - expect(forSnapshot(result.data.actors)).toMatchSnapshot() + const sorted = result.data.actors.sort((a, b) => + a.handle > b.handle ? 1 : -1, + ) + expect(forSnapshot(sorted)).toMatchSnapshot() }) it('search gives empty result set when provided empty term', async () => { @@ -191,7 +197,13 @@ describe('pds actor search views', () => { ) expect(full.data.actors.length).toBeGreaterThan(5) - expect(results(paginatedAll)).toEqual(results([full.data])) + const sortedFull = results([full.data]).sort((a, b) => + a.handle > b.handle ? 1 : -1, + ) + const sortedPaginated = results(paginatedAll).sort((a, b) => + a.handle > b.handle ? 1 : -1, + ) + expect(sortedPaginated).toEqual(sortedFull) }) it('search handles bad input', async () => { diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index a54bfde5359..5d589335346 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -48,7 +48,7 @@ export class TestNetwork extends TestNetworkNoAppView { ...params.pds, }) - mockNetworkUtilities(pds) + mockNetworkUtilities(pds, bsky) return new TestNetwork(plc, pds, bsky) } diff --git a/packages/dev-env/src/util.ts b/packages/dev-env/src/util.ts index 398e81b6c58..1535f0bbf55 100644 --- a/packages/dev-env/src/util.ts +++ b/packages/dev-env/src/util.ts @@ -1,13 +1,22 @@ -import { DidResolver, HandleResolver } from '@atproto/identity' +import { DidResolver, HandleResolver, IdResolver } from '@atproto/identity' import { TestPds } from './pds' +import { TestBsky } from './bsky' -export const mockNetworkUtilities = (pds: TestPds) => { +export const mockNetworkUtilities = async (pds: TestPds, bsky?: TestBsky) => { + await mockResolvers(pds.ctx.idResolver, pds) + if (bsky) { + await mockResolvers(bsky.ctx.idResolver, pds) + } +} + +export const mockResolvers = (idResolver: IdResolver, pds: TestPds) => { // Map pds public url to its local url when resolving from plc - const origResolveDid = DidResolver.prototype.resolveNoCache - DidResolver.prototype.resolveNoCache = async function (did) { - const result = await (origResolveDid.call(this, did) as ReturnType< - typeof origResolveDid - >) + const origResolveDid = idResolver.did.resolveNoCache + idResolver.did.resolveNoCache = async (did: string) => { + const result = await (origResolveDid.call( + idResolver.did, + did, + ) as ReturnType) const service = result?.service?.find((svc) => svc.id === '#atproto_pds') if (typeof service?.serviceEndpoint === 'string') { service.serviceEndpoint = service.serviceEndpoint.replace( @@ -18,7 +27,7 @@ export const mockNetworkUtilities = (pds: TestPds) => { return result } - HandleResolver.prototype.resolve = async function (handle: string) { + idResolver.handle.resolve = async (handle: string) => { const isPdsHandle = pds.ctx.cfg.availableUserDomains.some((domain) => handle.endsWith(domain), ) diff --git a/packages/identifier/src/handle.ts b/packages/identifier/src/handle.ts index 8eb029c0cff..12a4793f785 100644 --- a/packages/identifier/src/handle.ts +++ b/packages/identifier/src/handle.ts @@ -1,5 +1,7 @@ import { reservedSubdomains } from './reserved' +export const INVALID_HANDLE = 'handle.invalid' + // Currently these are registration-time restrictions, not protocol-level // restrictions. We have a couple accounts in the wild that we need to clean up // before hard-disallow. diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index 7d33a7a48b1..b12c3cffad3 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -224,6 +224,10 @@ export const forSnapshot = (obj: unknown) => { if (str.match(/^\d+::bafy/)) { return constantKeysetCursor } + + if (str.match(/^\d+::did:plc/)) { + return constantDidCursor + } if (str.match(/\/image\/[^/]+\/.+\/did:plc:[^/]+\/[^/]+@[\w]+$/)) { // Match image urls const match = str.match( @@ -292,6 +296,7 @@ export function take( export const constantDate = new Date(0).toISOString() export const constantKeysetCursor = '0000000000000::bafycid' +export const constantDidCursor = '0000000000000::did' const mapLeafValues = (obj: unknown, fn: (val: unknown) => unknown) => { if (Array.isArray(obj)) { diff --git a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap index 3bb5431cd84..64792c40b71 100644 --- a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap @@ -94,108 +94,103 @@ Object { `; exports[`proxies view requests actor.searchActor 1`] = ` -Object { - "actors": Array [ - Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", - "description": "its me!", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, +Array [ + Object { + "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "description": "its me!", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, }, - Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", - "description": "hi im bob label_me", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", - "muted": false, - }, + }, + Object { + "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "description": "hi im bob label_me", + "did": "user(2)", + "displayName": "bobby", + "handle": "bob.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(1)", + "following": "record(0)", + "muted": false, }, - Object { - "did": "user(4)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, + }, + Object { + "did": "user(4)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(3)", + "following": "record(2)", + "muted": false, }, - Object { - "did": "user(5)", - "handle": "dan.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, + }, + Object { + "did": "user(5)", + "handle": "dan.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "following": "record(4)", + "muted": false, }, - ], - "cursor": "0::dan.test", -} + }, +] `; exports[`proxies view requests actor.searchActorTypeahead 1`] = ` -Object { - "actors": Array [ - Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "viewer": Object { - "blockedBy": false, - "muted": false, - }, +Array [ + Object { + "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "viewer": Object { + "blockedBy": false, + "muted": false, }, - Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", - "viewer": Object { - "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", - "muted": false, - }, + }, + Object { + "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "did": "user(2)", + "displayName": "bobby", + "handle": "bob.test", + "viewer": Object { + "blockedBy": false, + "followedBy": "record(1)", + "following": "record(0)", + "muted": false, }, - Object { - "did": "user(4)", - "handle": "carol.test", - "viewer": Object { - "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", - "muted": false, - }, + }, + Object { + "did": "user(4)", + "handle": "carol.test", + "viewer": Object { + "blockedBy": false, + "followedBy": "record(3)", + "following": "record(2)", + "muted": false, }, - Object { - "did": "user(5)", - "handle": "dan.test", - "viewer": Object { - "blockedBy": false, - "following": "record(4)", - "muted": false, - }, + }, + Object { + "did": "user(5)", + "handle": "dan.test", + "viewer": Object { + "blockedBy": false, + "following": "record(4)", + "muted": false, }, - ], -} + }, +] `; exports[`proxies view requests feed.getAuthorFeed 1`] = ` diff --git a/packages/pds/tests/proxied/views.test.ts b/packages/pds/tests/proxied/views.test.ts index fec0e13cd20..ff5336c3e5f 100644 --- a/packages/pds/tests/proxied/views.test.ts +++ b/packages/pds/tests/proxied/views.test.ts @@ -103,7 +103,11 @@ describe('proxies view requests', () => { headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, }, ) - expect(forSnapshot(res.data)).toMatchSnapshot() + // sort because pagination is done off of did + const sortedFull = res.data.actors.sort((a, b) => + a.handle > b.handle ? 1 : -1, + ) + expect(forSnapshot(sortedFull)).toMatchSnapshot() const pt1 = await agent.api.app.bsky.actor.searchActors( { term: '.test', @@ -122,7 +126,10 @@ describe('proxies view requests', () => { headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, }, ) - expect([...pt1.data.actors, ...pt2.data.actors]).toEqual(res.data.actors) + const sortedPaginated = [...pt1.data.actors, ...pt2.data.actors].sort( + (a, b) => (a.handle > b.handle ? 1 : -1), + ) + expect(sortedPaginated).toEqual(sortedFull) }) it('actor.searchActorTypeahead', async () => { @@ -134,7 +141,10 @@ describe('proxies view requests', () => { headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, }, ) - expect(forSnapshot(res.data)).toMatchSnapshot() + const sorted = res.data.actors.sort((a, b) => + a.handle > b.handle ? 1 : -1, + ) + expect(forSnapshot(sorted)).toMatchSnapshot() }) it('feed.getAuthorFeed', async () => { diff --git a/packages/pds/tests/views/actor-search.test.ts b/packages/pds/tests/views/actor-search.test.ts index 47753a8de6b..5cb778db4a7 100644 --- a/packages/pds/tests/views/actor-search.test.ts +++ b/packages/pds/tests/views/actor-search.test.ts @@ -72,10 +72,13 @@ describe('pds user search views', () => { shouldNotContain.forEach((handle) => expect(handles).not.toContain(handle)) + const sorted = result.data.actors.sort((a, b) => + a.handle > b.handle ? 1 : -1, + ) if (db.dialect === 'pg') { - expect(forSnapshot(result.data.actors)).toEqual(snapTypeaheadPg) + expect(forSnapshot(sorted)).toEqual(snapTypeaheadPg) } else { - expect(forSnapshot(result.data.actors)).toEqual(snapTypeaheadSqlite) + expect(forSnapshot(sorted)).toEqual(snapTypeaheadSqlite) } }) @@ -141,10 +144,13 @@ describe('pds user search views', () => { shouldNotContain.forEach((handle) => expect(handles).not.toContain(handle)) + const sorted = result.data.actors.sort((a, b) => + a.handle > b.handle ? 1 : -1, + ) if (db.dialect === 'pg') { - expect(forSnapshot(result.data.actors)).toEqual(snapSearchPg) + expect(forSnapshot(sorted)).toEqual(snapSearchPg) } else { - expect(forSnapshot(result.data.actors)).toEqual(snapSearchSqlite) + expect(forSnapshot(sorted)).toEqual(snapSearchSqlite) } }) @@ -178,7 +184,13 @@ describe('pds user search views', () => { ) expect(full.data.actors.length).toBeGreaterThan(5) - expect(results(paginatedAll)).toEqual(results([full.data])) + const sortedFull = results([full.data]).sort((a, b) => + a.handle > b.handle ? 1 : -1, + ) + const sortedPaginated = results(paginatedAll).sort((a, b) => + a.handle > b.handle ? 1 : -1, + ) + expect(sortedPaginated).toEqual(sortedFull) }) it('search handles bad input', async () => { @@ -229,52 +241,52 @@ const avatar = const snapTypeaheadPg = [ { did: 'user(0)', - handle: 'cara-wiegand69.test', + displayName: 'Carlton Abernathy IV', + handle: 'aliya-hodkiewicz.test', + avatar, viewer: { blockedBy: false, muted: false }, labels: [], }, { did: 'user(1)', - displayName: 'Carol Littel', - handle: 'eudora-dietrich4.test', - avatar, + handle: 'cara-wiegand69.test', viewer: { blockedBy: false, muted: false }, labels: [], }, { did: 'user(2)', - displayName: 'Sadie Carter', - handle: 'shane-torphy52.test', - avatar, + handle: 'carlos6.test', viewer: { blockedBy: false, muted: false }, labels: [], }, { did: 'user(3)', - displayName: 'Carlton Abernathy IV', - handle: 'aliya-hodkiewicz.test', + displayName: 'Latoya Windler', + handle: 'carolina-mcdermott77.test', avatar, viewer: { blockedBy: false, muted: false }, labels: [], }, { did: 'user(4)', - handle: 'carlos6.test', + displayName: 'Rachel Kshlerin', + handle: 'cayla-marquardt39.test', + avatar, viewer: { blockedBy: false, muted: false }, labels: [], }, { did: 'user(5)', - displayName: 'Latoya Windler', - handle: 'carolina-mcdermott77.test', + displayName: 'Carol Littel', + handle: 'eudora-dietrich4.test', avatar, viewer: { blockedBy: false, muted: false }, labels: [], }, { did: 'user(6)', - displayName: 'Rachel Kshlerin', - handle: 'cayla-marquardt39.test', + displayName: 'Sadie Carter', + handle: 'shane-torphy52.test', avatar, viewer: { blockedBy: false, muted: false }, labels: [], @@ -331,57 +343,57 @@ const snapTypeaheadSqlite = [ const snapSearchPg = [ { did: 'user(0)', - handle: 'cara-wiegand69.test', + displayName: 'Carlton Abernathy IV', + indexedAt: '1970-01-01T00:00:00.000Z', + handle: 'aliya-hodkiewicz.test', + avatar, viewer: { blockedBy: false, muted: false }, labels: [], }, { did: 'user(1)', - displayName: 'Carol Littel', - indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'eudora-dietrich4.test', - avatar, + handle: 'cara-wiegand69.test', viewer: { blockedBy: false, muted: false }, labels: [], }, { did: 'user(2)', - displayName: 'Sadie Carter', - indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'shane-torphy52.test', - avatar, + handle: 'carlos6.test', viewer: { blockedBy: false, muted: false }, labels: [], }, { did: 'user(3)', - displayName: 'Carlton Abernathy IV', + displayName: 'Latoya Windler', indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'aliya-hodkiewicz.test', + handle: 'carolina-mcdermott77.test', avatar, viewer: { blockedBy: false, muted: false }, labels: [], }, { did: 'user(4)', - handle: 'carlos6.test', + displayName: 'Rachel Kshlerin', + indexedAt: '1970-01-01T00:00:00.000Z', + handle: 'cayla-marquardt39.test', + avatar, viewer: { blockedBy: false, muted: false }, labels: [], }, { did: 'user(5)', - displayName: 'Latoya Windler', + displayName: 'Carol Littel', indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'carolina-mcdermott77.test', + handle: 'eudora-dietrich4.test', avatar, viewer: { blockedBy: false, muted: false }, labels: [], }, { did: 'user(6)', - displayName: 'Rachel Kshlerin', + displayName: 'Sadie Carter', indexedAt: '1970-01-01T00:00:00.000Z', - handle: 'cayla-marquardt39.test', + handle: 'shane-torphy52.test', avatar, viewer: { blockedBy: false, muted: false }, labels: [], From 2e4a114379b139b9cf56ac4b76966c8ecee2c701 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Fri, 30 Jun 2023 18:18:04 -0400 Subject: [PATCH 007/237] Remove post hierarchy indexing from bsky appview (#1257) * remove post_hierarchy from db model and indexing in bsky appview * update bsky appview getPostThread to use recursive query to build thread * add covering index to speed-up descendents query * tidy post/notification processing w/o post_hierarchy * tidy, disallow infinitely following reply cycles --- .../src/api/app/bsky/feed/getPostThread.ts | 42 ++-- packages/bsky/src/db/database-schema.ts | 2 - ...230629T220835893Z-remove-post-hierarchy.ts | 32 +++ packages/bsky/src/db/migrations/index.ts | 1 + packages/bsky/src/db/tables/post-hierarchy.ts | 11 -- .../src/services/indexing/plugins/post.ts | 187 ++++++------------ packages/bsky/src/services/util/post.ts | 65 ++++++ packages/bsky/tests/seeds/thread.ts | 40 ---- packages/bsky/tests/views/thread.test.ts | 65 +----- 9 files changed, 183 insertions(+), 262 deletions(-) create mode 100644 packages/bsky/src/db/migrations/20230629T220835893Z-remove-post-hierarchy.ts delete mode 100644 packages/bsky/src/db/tables/post-hierarchy.ts create mode 100644 packages/bsky/src/services/util/post.ts delete mode 100644 packages/bsky/tests/seeds/thread.ts diff --git a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts index ce019bef047..fc1723f27f7 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts @@ -15,6 +15,10 @@ import { ThreadViewPost, isNotFoundPost, } from '../../../../lexicon/types/app/bsky/feed/defs' +import { + getAncestorsAndSelfQb, + getDescendentsQb, +} from '../../../../services/util/post' export type PostThread = { post: FeedRow @@ -32,12 +36,7 @@ export default function (server: Server, ctx: AppContext) { const feedService = ctx.services.feed(ctx.db) const labelService = ctx.services.label(ctx.db) - const threadData = await getThreadData( - feedService, - uri, - depth, - parentHeight, - ) + const threadData = await getThreadData(ctx, uri, depth, parentHeight) if (!threadData) { throw new InvalidRequestError(`Post not found: ${uri}`, 'NotFound') } @@ -173,24 +172,31 @@ const getRelevantIds = ( } const getThreadData = async ( - feedService: FeedService, + ctx: AppContext, uri: string, depth: number, parentHeight: number, ): Promise => { + const feedService = ctx.services.feed(ctx.db) const [parents, children] = await Promise.all([ - feedService - .selectPostQb() - .innerJoin('post_hierarchy', 'post_hierarchy.ancestorUri', 'post.uri') - .where('post_hierarchy.uri', '=', uri) + getAncestorsAndSelfQb(ctx.db.db, { uri, parentHeight }) + .selectFrom('ancestor') + .innerJoin( + feedService.selectPostQb().as('post'), + 'post.uri', + 'ancestor.uri', + ) + .selectAll('post') .execute(), - feedService - .selectPostQb() - .innerJoin('post_hierarchy', 'post_hierarchy.uri', 'post.uri') - .where('post_hierarchy.uri', '!=', uri) - .where('post_hierarchy.ancestorUri', '=', uri) - .where('depth', '<=', depth) - .orderBy('post.createdAt', 'desc') + getDescendentsQb(ctx.db.db, { uri, depth }) + .selectFrom('descendent') + .innerJoin( + feedService.selectPostQb().as('post'), + 'post.uri', + 'descendent.uri', + ) + .selectAll('post') + .orderBy('sortAt', 'desc') .execute(), ]) const parentsByUri = parents.reduce((acc, parent) => { diff --git a/packages/bsky/src/db/database-schema.ts b/packages/bsky/src/db/database-schema.ts index e9c097e866b..16547eb94df 100644 --- a/packages/bsky/src/db/database-schema.ts +++ b/packages/bsky/src/db/database-schema.ts @@ -4,7 +4,6 @@ import * as profile from './tables/profile' import * as profileAgg from './tables/profile-agg' import * as post from './tables/post' import * as postEmbed from './tables/post-embed' -import * as postHierarchy from './tables/post-hierarchy' import * as postAgg from './tables/post-agg' import * as repost from './tables/repost' import * as feedItem from './tables/feed-item' @@ -34,7 +33,6 @@ export type DatabaseSchemaType = duplicateRecord.PartialDB & profileAgg.PartialDB & post.PartialDB & postEmbed.PartialDB & - postHierarchy.PartialDB & postAgg.PartialDB & repost.PartialDB & feedItem.PartialDB & diff --git a/packages/bsky/src/db/migrations/20230629T220835893Z-remove-post-hierarchy.ts b/packages/bsky/src/db/migrations/20230629T220835893Z-remove-post-hierarchy.ts new file mode 100644 index 00000000000..36d9b7cd37d --- /dev/null +++ b/packages/bsky/src/db/migrations/20230629T220835893Z-remove-post-hierarchy.ts @@ -0,0 +1,32 @@ +import { Kysely, sql } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema.dropTable('post_hierarchy').execute() + // recreate index that calculates e.g. "replyCount", turning it into a covering index + // for uri so that recursive query for post descendents can use an index-only scan. + await db.schema.dropIndex('post_replyparent_idx').execute() + await sql`create index "post_replyparent_idx" on "post" ("replyParent") include ("uri")`.execute( + db, + ) +} + +export async function down(db: Kysely): Promise { + await db.schema + .createTable('post_hierarchy') + .addColumn('uri', 'varchar', (col) => col.notNull()) + .addColumn('ancestorUri', 'varchar', (col) => col.notNull()) + .addColumn('depth', 'integer', (col) => col.notNull()) + .addPrimaryKeyConstraint('post_hierarchy_pkey', ['uri', 'ancestorUri']) + .execute() + await db.schema + .createIndex('post_hierarchy_ancestoruri_idx') + .on('post_hierarchy') + .column('ancestorUri') + .execute() + await db.schema.dropIndex('post_replyparent_idx').execute() + await db.schema + .createIndex('post_replyparent_idx') + .on('post') + .column('replyParent') + .execute() +} diff --git a/packages/bsky/src/db/migrations/index.ts b/packages/bsky/src/db/migrations/index.ts index 1bbc9cf8f34..487e61b7cf5 100644 --- a/packages/bsky/src/db/migrations/index.ts +++ b/packages/bsky/src/db/migrations/index.ts @@ -18,3 +18,4 @@ export * as _20230610T203555962Z from './20230610T203555962Z-suggested-follows' export * as _20230611T215300060Z from './20230611T215300060Z-actor-state' export * as _20230620T161134972Z from './20230620T161134972Z-post-langs' export * as _20230627T212437895Z from './20230627T212437895Z-optional-handle' +export * as _20230629T220835893Z from './20230629T220835893Z-remove-post-hierarchy' diff --git a/packages/bsky/src/db/tables/post-hierarchy.ts b/packages/bsky/src/db/tables/post-hierarchy.ts deleted file mode 100644 index ba7e11ccb1b..00000000000 --- a/packages/bsky/src/db/tables/post-hierarchy.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const tableName = 'post_hierarchy' - -export interface PostHierarchy { - uri: string - ancestorUri: string - depth: number -} - -export type PartialDB = { - [tableName]: PostHierarchy -} diff --git a/packages/bsky/src/services/indexing/plugins/post.ts b/packages/bsky/src/services/indexing/plugins/post.ts index 35028020069..ec62ca24e17 100644 --- a/packages/bsky/src/services/indexing/plugins/post.ts +++ b/packages/bsky/src/services/indexing/plugins/post.ts @@ -13,28 +13,43 @@ import { import * as lex from '../../../lexicon/lexicons' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' -import { PostHierarchy } from '../../../db/tables/post-hierarchy' import { Notification } from '../../../db/tables/notification' import { toSimplifiedISOSafe } from '../util' import Database from '../../../db' import { countAll, excluded } from '../../../db/util' import { BackgroundQueue } from '../../../background' +import { getAncestorsAndSelfQb, getDescendentsQb } from '../../util/post' type Notif = Insertable type Post = Selectable type PostEmbedImage = DatabaseSchemaType['post_embed_image'] type PostEmbedExternal = DatabaseSchemaType['post_embed_external'] type PostEmbedRecord = DatabaseSchemaType['post_embed_record'] +type PostAncestor = { + uri: string + height: number +} +type PostDescendent = { + uri: string + depth: number + cid: string + creator: string + indexedAt: string +} type IndexedPost = { post: Post facets?: { type: 'mention' | 'link'; value: string }[] embeds?: (PostEmbedImage[] | PostEmbedExternal | PostEmbedRecord)[] - ancestors?: PostHierarchy[] - descendents?: IndexedPost[] + ancestors?: PostAncestor[] + descendents?: PostDescendent[] } const lexId = lex.ids.AppBskyFeedPost +const REPLY_NOTIF_DEPTH = 5 +const BLESSED_HELL_THREAD = + 'at://did:plc:wgaezxqi2spqm3mhrb5xvkzi/app.bsky.feed.post/3juzlwllznd24' + const insertFn = async ( db: DatabaseSchema, uri: AtUri, @@ -135,94 +150,31 @@ const insertFn = async ( await db.insertInto('post_embed_record').values(recordEmbed).execute() } } - // Thread indexing - // Concurrent, out-of-order updates are difficult, so we essentially - // take a lock on the thread for indexing, by locking the thread's root post. - await db - .selectFrom('post') - .forUpdate() + const ancestors = await getAncestorsAndSelfQb(db, { + uri: post.uri, + parentHeight: + post.replyRoot === BLESSED_HELL_THREAD ? 100 : REPLY_NOTIF_DEPTH, + }) + .selectFrom('ancestor') .selectAll() - .where('uri', '=', post.replyRoot ?? post.uri) - .execute() - - // Track the minimum we know about the post hierarchy: the post and its parent. - // Importantly, this works even if the parent hasn't been indexed yet. - const minimalPostHierarchy = [ - { - uri: post.uri, - ancestorUri: post.uri, - depth: 0, - }, - ] - if (post.replyParent) { - minimalPostHierarchy.push({ - uri: post.uri, - ancestorUri: post.replyParent, - depth: 1, - }) - } - const ancestors = await db - .insertInto('post_hierarchy') - .values(minimalPostHierarchy) - .onConflict((oc) => oc.doNothing()) - .returningAll() .execute() - - // Copy all the parent's relations down to the post. - // It's possible that the parent hasn't been indexed yet and this will no-op. - if (post.replyParent) { - const deepAncestors = await db - .insertInto('post_hierarchy') - .columns(['uri', 'ancestorUri', 'depth']) - .expression( - db - .selectFrom('post_hierarchy as parent_hierarchy') - .where('parent_hierarchy.uri', '=', post.replyParent) - .select([ - sql`${post.uri}`.as('uri'), - 'ancestorUri', - sql`depth + 1`.as('depth'), - ]), - ) - .onConflict((oc) => oc.doNothing()) - .returningAll() - .execute() - ancestors.push(...deepAncestors) - } - - // Copy all post's relations down to its descendents. This ensures - // that out-of-order indexing (i.e. descendent before parent) is resolved. - const descendents = await db - .insertInto('post_hierarchy') - .columns(['uri', 'ancestorUri', 'depth']) - .expression( - db - .selectFrom('post_hierarchy as target') - .innerJoin( - 'post_hierarchy as source', - 'source.ancestorUri', - 'target.uri', - ) - .where('target.uri', '=', post.uri) - .select([ - 'source.uri as uri', - 'target.ancestorUri as ancestorUri', - sql`source.depth + target.depth`.as('depth'), - ]), - ) - .onConflict((oc) => oc.doNothing()) - .returningAll() + const descendents = await getDescendentsQb(db, { + uri: post.uri, + depth: post.replyRoot === BLESSED_HELL_THREAD ? 100 : REPLY_NOTIF_DEPTH, + }) + .selectFrom('descendent') + .innerJoin('post', 'post.uri', 'descendent.uri') + .selectAll('descendent') + .select(['cid', 'creator', 'indexedAt']) .execute() - return { post: insertedPost, facets, embeds, ancestors, - descendents: await collateDescendents(db, descendents), + descendents, } - // return { post: insertedPost, facets, embeds, ancestors } } const findDuplicate = async (): Promise => { @@ -267,14 +219,13 @@ const notifsForInsert = (obj: IndexedPost) => { } } - const ancestors = (obj.ancestors ?? []) - .filter((a) => a.depth > 0) // no need to notify self - .sort((a, b) => a.depth - b.depth) - const BLESSED_HELL_THREAD = - 'at://did:plc:wgaezxqi2spqm3mhrb5xvkzi/app.bsky.feed.post/3juzlwllznd24' - for (const relation of ancestors) { - if (relation.depth < 5 || obj.post.replyRoot === BLESSED_HELL_THREAD) { - const ancestorUri = new AtUri(relation.ancestorUri) + for (const ancestor of obj.ancestors ?? []) { + if (ancestor.uri === obj.post.uri) continue // no need to notify for own post + if ( + ancestor.height < REPLY_NOTIF_DEPTH || + obj.post.replyRoot === BLESSED_HELL_THREAD + ) { + const ancestorUri = new AtUri(ancestor.uri) maybeNotify({ did: ancestorUri.host, reason: 'reply', @@ -287,10 +238,26 @@ const notifsForInsert = (obj: IndexedPost) => { } } - if (obj.descendents) { - // May generate notifications for out-of-order indexing of replies - for (const descendent of obj.descendents) { - notifs.push(...notifsForInsert(descendent)) + // descendents indicate out-of-order indexing: need to notify + // the current post and upwards. + for (const descendent of obj.descendents ?? []) { + for (const ancestor of obj.ancestors ?? []) { + const totalHeight = descendent.depth + ancestor.height + if ( + totalHeight < REPLY_NOTIF_DEPTH || + obj.post.replyRoot === BLESSED_HELL_THREAD + ) { + const ancestorUri = new AtUri(ancestor.uri) + maybeNotify({ + did: ancestorUri.host, + reason: 'reply', + reasonSubject: ancestorUri.toString(), + author: descendent.creator, + recordUri: descendent.uri, + recordCid: descendent.cid, + sortAt: descendent.indexedAt, + }) + } } } @@ -341,19 +308,11 @@ const deleteFn = async ( if (deletedPosts) { deletedEmbeds.push(deletedPosts) } - // Do not delete, maintain thread hierarchy even if post no longer exists - const ancestors = await db - .selectFrom('post_hierarchy') - .where('uri', '=', uriStr) - .where('depth', '>', 0) - .selectAll() - .execute() return deleted ? { post: deleted, facets: [], // Not used embeds: deletedEmbeds, - ancestors, } : null } @@ -429,29 +388,3 @@ function separateEmbeds(embed: PostRecord['embed']) { } return [embed] } - -async function collateDescendents( - db: DatabaseSchema, - descendents: PostHierarchy[], -): Promise { - if (!descendents.length) return - - const ancestorsByUri = descendents.reduce((acc, descendent) => { - acc[descendent.uri] ??= [] - acc[descendent.uri].push(descendent) - return acc - }, {} as Record) - - const descendentPosts = await db - .selectFrom('post') - .selectAll() - .where('uri', 'in', Object.keys(ancestorsByUri)) - .execute() - - return descendentPosts.map((post) => { - return { - post, - ancestors: ancestorsByUri[post.uri], - } - }) -} diff --git a/packages/bsky/src/services/util/post.ts b/packages/bsky/src/services/util/post.ts new file mode 100644 index 00000000000..19e7fa3ee2c --- /dev/null +++ b/packages/bsky/src/services/util/post.ts @@ -0,0 +1,65 @@ +import { sql } from 'kysely' +import DatabaseSchema from '../../db/database-schema' + +export const getDescendentsQb = ( + db: DatabaseSchema, + opts: { + uri: string + depth: number // required, protects against cycles + }, +) => { + const { uri, depth } = opts + const query = db.withRecursive('descendent(uri, depth)', (cte) => { + return cte + .selectFrom('post') + .select(['post.uri as uri', sql`1`.as('depth')]) + .where(sql`1`, '<=', depth) + .where('replyParent', '=', uri) + .unionAll( + cte + .selectFrom('post') + .innerJoin('descendent', 'descendent.uri', 'post.replyParent') + .where('descendent.depth', '<', depth) + .select([ + 'post.uri as uri', + sql`descendent.depth + 1`.as('depth'), + ]), + ) + }) + return query +} + +export const getAncestorsAndSelfQb = ( + db: DatabaseSchema, + opts: { + uri: string + parentHeight: number // required, protects against cycles + }, +) => { + const { uri, parentHeight } = opts + const query = db.withRecursive( + 'ancestor(uri, ancestorUri, height)', + (cte) => { + return cte + .selectFrom('post') + .select([ + 'post.uri as uri', + 'post.replyParent as ancestorUri', + sql`0`.as('height'), + ]) + .where('uri', '=', uri) + .unionAll( + cte + .selectFrom('post') + .innerJoin('ancestor', 'ancestor.ancestorUri', 'post.uri') + .where('ancestor.height', '<', parentHeight) + .select([ + 'post.uri as uri', + 'post.replyParent as ancestorUri', + sql`ancestor.height + 1`.as('height'), + ]), + ) + }, + ) + return query +} diff --git a/packages/bsky/tests/seeds/thread.ts b/packages/bsky/tests/seeds/thread.ts deleted file mode 100644 index 921736e919e..00000000000 --- a/packages/bsky/tests/seeds/thread.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { RecordRef, SeedClient } from './client' - -export default async (sc: SeedClient, did, threads: Item[]) => { - const refByItemId: Record = {} - const rootByItemId: Record = {} - await walk(threads, async (item, _depth, parent) => { - if (parent !== undefined) { - const parentRef = refByItemId[parent.id] - const rootRef = rootByItemId[parent.id] - const { ref } = await sc.reply(did, rootRef, parentRef, String(item.id)) - refByItemId[item.id] = ref - rootByItemId[item.id] = rootRef - } else { - const { ref } = await sc.post(did, String(item.id)) - refByItemId[item.id] = ref - rootByItemId[item.id] = ref - } - }) -} - -export function item(id: number, children: Item[] = []) { - return { id, children } -} - -export async function walk( - items: Item[], - cb: (item: Item, depth: number, parent?: Item) => Promise, - depth = 0, - parent?: Item, -) { - for (const item of items) { - await cb(item, depth, parent) - await walk(item.children, cb, depth + 1, item) - } -} - -export interface Item { - id: number - children: Item[] -} diff --git a/packages/bsky/tests/views/thread.test.ts b/packages/bsky/tests/views/thread.test.ts index af23c189ec4..c3f5cb274dc 100644 --- a/packages/bsky/tests/views/thread.test.ts +++ b/packages/bsky/tests/views/thread.test.ts @@ -1,16 +1,13 @@ import AtpAgent, { AppBskyFeedGetPostThread } from '@atproto/api' import { TestNetwork } from '@atproto/dev-env' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' -import { Database } from '../../src' import { forSnapshot, stripViewerFromThread } from '../_util' -import { RecordRef, SeedClient } from '../seeds/client' +import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' -import threadSeed, { walk, item, Item } from '../seeds/thread' describe('pds thread views', () => { let network: TestNetwork let agent: AtpAgent - let db: Database let sc: SeedClient // account dids, for convenience @@ -22,7 +19,6 @@ describe('pds thread views', () => { network = await TestNetwork.create({ dbPostgresSchema: 'bsky_views_thread', }) - db = network.bsky.ctx.db agent = network.bsky.getClient() const pdsAgent = network.pds.getClient() sc = new SeedClient(pdsAgent) @@ -145,65 +141,6 @@ describe('pds thread views', () => { expect(forSnapshot(thread3.data.thread)).toMatchSnapshot() }) - it('builds post hierarchy index.', async () => { - const threads: Item[] = [ - item(1, [item(2, [item(3), item(4)])]), - item(5, [item(6), item(7, [item(9, [item(11)]), item(10)]), item(8)]), - item(12), - ] - - await threadSeed(sc, sc.dids.alice, threads) - await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() - - let closureSize = 0 - const itemByUri: Record = {} - - const postsAndReplies = ([] as { text: string; ref: RecordRef }[]) - .concat(Object.values(sc.posts[sc.dids.alice])) - .concat(Object.values(sc.replies[sc.dids.alice])) - .filter((p) => { - const id = parseInt(p.text, 10) - return 0 < id && id <= 12 - }) - - await walk(threads, async (item, depth) => { - const post = postsAndReplies.find((p) => p.text === String(item.id)) - if (!post) throw new Error('Post not found') - itemByUri[post.ref.uriStr] = item - closureSize += depth + 1 - }) - - const hierarchy = await db.db - .selectFrom('post_hierarchy') - .where( - 'uri', - 'in', - postsAndReplies.map((p) => p.ref.uriStr), - ) - .orWhere( - 'ancestorUri', - 'in', - postsAndReplies.map((p) => p.ref.uriStr), - ) - .selectAll() - .execute() - - expect(hierarchy.length).toEqual(closureSize) - - for (const relation of hierarchy) { - const item = itemByUri[relation.uri] - const ancestor = itemByUri[relation.ancestorUri] - let depth = -1 - await walk([ancestor], async (candidate, candidateDepth) => { - if (candidate === item) { - depth = candidateDepth - } - }) - expect(depth).toEqual(relation.depth) - } - }) - describe('takedown', () => { it('blocks post by actor', async () => { const { data: modAction } = From e5dc790eda26cb0ec639f2456ce6152d4005cf25 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Sun, 2 Jul 2023 20:22:35 -0500 Subject: [PATCH 008/237] Ensure not to serve repo endpoints for taken-down actor (#1263) ensure not to serve repo endpoints for taken-down actor Co-authored-by: Devin Ivy --- packages/pds/src/services/account/index.ts | 8 ++++- packages/pds/tests/crud.test.ts | 40 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index 48902acec96..5038a91295e 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -82,7 +82,13 @@ export class AccountService { handleOrDid: string, includeSoftDeleted = false, ): Promise { - if (handleOrDid.startsWith('did:')) return handleOrDid + if (handleOrDid.startsWith('did:')) { + if (includeSoftDeleted) { + return handleOrDid + } + const available = await this.isRepoAvailable(handleOrDid) + return available ? handleOrDid : null + } const { ref } = this.db.db.dynamic const found = await this.db.db .selectFrom('did_handle') diff --git a/packages/pds/tests/crud.test.ts b/packages/pds/tests/crud.test.ts index fee0007ea3f..d4425d72586 100644 --- a/packages/pds/tests/crud.test.ts +++ b/packages/pds/tests/crud.test.ts @@ -1160,6 +1160,46 @@ describe('crud operations', () => { }, ) }) + + it("doesn't serve taken-down actor", async () => { + const posts = await agent.api.app.bsky.feed.post.list({ repo: alice.did }) + expect(posts.records.length).toBeGreaterThan(0) + + const { data: action } = + await agent.api.com.atproto.admin.takeModerationAction( + { + action: TAKEDOWN, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: alice.did, + }, + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: { authorization: adminAuth() }, + }, + ) + + const tryListPosts = agent.api.app.bsky.feed.post.list({ + repo: alice.did, + }) + await expect(tryListPosts).rejects.toThrow(/Could not find repo/) + + // Cleanup + await agent.api.com.atproto.admin.reverseModerationAction( + { + id: action.id, + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: { authorization: adminAuth() }, + }, + ) + }) }) function createDeepObject(depth: number) { From 4db7aeca574871ea3f8365635e742fc4c4bdfbf8 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Sun, 2 Jul 2023 20:37:27 -0500 Subject: [PATCH 009/237] Rework record embeds (#1262) * wip wip wip fix tests tidy * small fix * fix flaky proxy test --- .../api/app/bsky/feed/getFeedGenerator.ts | 4 +- .../api/app/bsky/feed/getFeedGenerators.ts | 6 +- .../api/app/bsky/feed/getPostThread.ts | 16 +- .../app-view/api/app/bsky/feed/getPosts.ts | 24 +- .../src/app-view/api/app/bsky/unspecced.ts | 4 +- .../pds/src/app-view/services/feed/index.ts | 399 ++++++++---------- .../pds/src/app-view/services/feed/types.ts | 40 +- .../pds/src/app-view/services/feed/views.ts | 116 ++++- packages/pds/tests/proxied/procedures.test.ts | 1 + 9 files changed, 347 insertions(+), 263 deletions(-) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts index 1e7ebff6aca..ec874565ebe 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts @@ -27,7 +27,7 @@ export default function (server: Server, ctx: AppContext) { const feedService = ctx.services.appView.feed(ctx.db) - const got = await feedService.getFeedGeneratorViews([feed], requester) + const got = await feedService.getFeedGeneratorInfos([feed], requester) const feedInfo = got[feed] if (!feedInfo) { throw new InvalidRequestError('could not find feed') @@ -56,7 +56,7 @@ export default function (server: Server, ctx: AppContext) { ) } - const profiles = await feedService.getActorViews( + const profiles = await feedService.getActorInfos( [feedInfo.creator], requester, ) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts index 3052c642a46..a7879dd0934 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts @@ -21,11 +21,11 @@ export default function (server: Server, ctx: AppContext) { const feedService = ctx.services.appView.feed(ctx.db) - const genViews = await feedService.getFeedGeneratorViews(feeds, requester) - const genList = Object.values(genViews) + const genInfos = await feedService.getFeedGeneratorInfos(feeds, requester) + const genList = Object.values(genInfos) const creators = genList.map((gen) => gen.creator) - const profiles = await feedService.getActorViews(creators, requester) + const profiles = await feedService.getActorInfos(creators, requester) const feedViews = genList.map((gen) => feedService.views.formatFeedGeneratorView(gen, profiles), diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts index ce1c17b5ea5..90244bf2e3f 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts @@ -2,8 +2,8 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../../lexicon' import AppContext from '../../../../../context' import { - ActorViewMap, - FeedEmbeds, + ActorInfoMap, + PostEmbedViews, FeedRow, FeedService, PostInfoMap, @@ -53,14 +53,14 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Post not found: ${uri}`, 'NotFound') } const relevant = getRelevantIds(threadData) - const [actors, posts, embeds, labels] = await Promise.all([ - feedService.getActorViews(Array.from(relevant.dids), requester, { + const [actors, posts, labels] = await Promise.all([ + feedService.getActorInfos(Array.from(relevant.dids), requester, { skipLabels: true, }), - feedService.getPostViews(Array.from(relevant.uris), requester), - feedService.embedsForPosts(Array.from(relevant.uris), requester), + feedService.getPostInfos(Array.from(relevant.uris), requester), labelService.getLabelsForSubjects([...relevant.uris, ...relevant.dids]), ]) + const embeds = await feedService.embedsForPosts(posts, requester) const thread = composeThread( threadData, @@ -88,8 +88,8 @@ const composeThread = ( threadData: PostThread, feedService: FeedService, posts: PostInfoMap, - actors: ActorViewMap, - embeds: FeedEmbeds, + actors: ActorInfoMap, + embeds: PostEmbedViews, labels: Labels, ): ThreadViewPost | NotFoundPost | BlockedPost => { const post = feedService.views.formatPostView( diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts index 0f5df994e8e..cc6c39ec44b 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts @@ -1,7 +1,6 @@ import * as common from '@atproto/common' import { Server } from '../../../../../lexicon' import AppContext from '../../../../../context' -import { AtUri } from '@atproto/uri' import { PostView } from '@atproto/api/src/client/types/app/bsky/feed/defs' export default function (server: Server, ctx: AppContext) { @@ -20,30 +19,15 @@ export default function (server: Server, ctx: AppContext) { } } - const feedService = ctx.services.appView.feed(ctx.db) - const labelService = ctx.services.appView.label(ctx.db) - const uris = common.dedupeStrs(params.uris) - const dids = common.dedupeStrs( - params.uris.map((uri) => new AtUri(uri).hostname), - ) - const [actors, postViews, embeds, labels] = await Promise.all([ - feedService.getActorViews(dids, requester, { skipLabels: true }), - feedService.getPostViews(uris, requester), - feedService.embedsForPosts(uris, requester), - labelService.getLabelsForSubjects([...uris, ...dids]), - ]) + const postViews = await ctx.services.appView + .feed(ctx.db) + .getPostViews(uris, requester) const posts: PostView[] = [] for (const uri of uris) { - const post = feedService.views.formatPostView( - uri, - actors, - postViews, - embeds, - labels, - ) + const post = postViews[uri] if (post) { posts.push(post) } diff --git a/packages/pds/src/app-view/api/app/bsky/unspecced.ts b/packages/pds/src/app-view/api/app/bsky/unspecced.ts index defa197b709..12b6db691eb 100644 --- a/packages/pds/src/app-view/api/app/bsky/unspecced.ts +++ b/packages/pds/src/app-view/api/app/bsky/unspecced.ts @@ -147,14 +147,14 @@ export default function (server: Server, ctx: AppContext) { .limit(50) .execute() - const genViews = await feedService.getFeedGeneratorViews( + const genViews = await feedService.getFeedGeneratorInfos( mostPopularFeeds.map((feed) => feed.uri), requester, ) const genList = Object.values(genViews) const creators = genList.map((gen) => gen.creator) - const profiles = await feedService.getActorViews(creators, requester) + const profiles = await feedService.getActorInfos(creators, requester) const feedViews = genList.map((gen) => feedService.views.formatFeedGeneratorView(gen, profiles), diff --git a/packages/pds/src/app-view/services/feed/index.ts b/packages/pds/src/app-view/services/feed/index.ts index e39c445b943..326588efe36 100644 --- a/packages/pds/src/app-view/services/feed/index.ts +++ b/packages/pds/src/app-view/services/feed/index.ts @@ -5,30 +5,30 @@ import Database from '../../../db' import { countAll, notSoftDeletedClause } from '../../../db/util' import { ImageUriBuilder } from '../../../image/uri' import { ids } from '../../../lexicon/lexicons' -import { isView as isViewImages } from '../../../lexicon/types/app/bsky/embed/images' -import { isView as isViewExternal } from '../../../lexicon/types/app/bsky/embed/external' import { - ViewBlocked, - ViewNotFound, - ViewRecord, - View as RecordEmbedView, -} from '../../../lexicon/types/app/bsky/embed/record' + Record as PostRecord, + isRecord as isPostRecord, +} from '../../../lexicon/types/app/bsky/feed/post' +import { isMain as isEmbedImages } from '../../../lexicon/types/app/bsky/embed/images' +import { isMain as isEmbedExternal } from '../../../lexicon/types/app/bsky/embed/external' +import { isMain as isEmbedRecord } from '../../../lexicon/types/app/bsky/embed/record' +import { isMain as isEmbedRecordWithMedia } from '../../../lexicon/types/app/bsky/embed/recordWithMedia' +import { FeedViewPost } from '../../../lexicon/types/app/bsky/feed/defs' import { - FeedViewPost, - PostView, -} from '../../../lexicon/types/app/bsky/feed/defs' -import { - ActorViewMap, - FeedEmbeds, + ActorInfoMap, PostInfoMap, FeedItemType, FeedRow, FeedGenInfoMap, + PostViews, + PostEmbedViews, + RecordEmbedViewRecordMap, } from './types' -import { LabelService } from '../label' +import { LabelService, Labels } from '../label' import { ActorService } from '../actor' import { GraphService } from '../graph' import { FeedViews } from './views' +import { cborToLexRecord } from '@atproto/repo' export * from './types' @@ -106,11 +106,11 @@ export class FeedService { } // @NOTE keep in sync with actorService.views.profile() - async getActorViews( + async getActorInfos( dids: string[], requester: string, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, // @NOTE used by hydrateFeed() to batch label hydration - ): Promise { + ): Promise { if (dids.length < 1) return {} const { ref } = this.db.db.dynamic const { skipLabels = false, includeSoftDeleted = false } = opts ?? {} @@ -187,10 +187,10 @@ export class FeedService { labels: skipLabels ? undefined : actorLabels, }, } - }, {} as ActorViewMap) + }, {} as ActorInfoMap) } - async getPostViews( + async getPostInfos( postUris: string[], requester: string, ): Promise { @@ -242,7 +242,7 @@ export class FeedService { ) } - async getFeedGeneratorViews(generatorUris: string[], requester: string) { + async getFeedGeneratorInfos(generatorUris: string[], requester: string) { if (generatorUris.length < 1) return {} const feedGens = await this.selectFeedGeneratorQb(requester) .where('feed_generator.uri', 'in', generatorUris) @@ -256,176 +256,36 @@ export class FeedService { ) } - async embedsForPosts( - uris: string[], + async getPostViews( + postUris: string[], requester: string, - _depth = 0, - ): Promise { - if (uris.length < 1 || _depth > 1) { - // If a post has a record embed which contains additional embeds, the depth check - // above ensures that we don't recurse indefinitely into those additional embeds. - // In short, you receive up to two layers of embeds for the post: this allows us to - // handle the case that a post has a record embed, which in turn has images embedded in it. - return {} - } - const imgPromise = this.db.db - .selectFrom('post_embed_image') - .selectAll() - .where('postUri', 'in', uris) - .orderBy('postUri') - .orderBy('position') - .execute() - const extPromise = this.db.db - .selectFrom('post_embed_external') - .selectAll() - .where('postUri', 'in', uris) - .execute() - const recordPromise = this.db.db - .selectFrom('post_embed_record') - .innerJoin('record as embed', 'embed.uri', 'embedUri') - .where('postUri', 'in', uris) - .select(['postUri', 'embed.uri as uri', 'embed.did as did']) - .execute() - const [images, externals, records] = await Promise.all([ - imgPromise, - extPromise, - recordPromise, - ]) - const nestedUris = dedupeStrs(records.map((p) => p.uri)) - const nestedDids = dedupeStrs(records.map((p) => p.did)) - const nestedPostUris = nestedUris.filter( - (uri) => new AtUri(uri).collection === ids.AppBskyFeedPost, - ) - const nestedFeedGenUris = nestedUris.filter( - (uri) => new AtUri(uri).collection === ids.AppBskyFeedGenerator, - ) - const nestedListUris = nestedUris.filter( - (uri) => new AtUri(uri).collection === ids.AppBskyGraphList, - ) - const [ - postViews, - actorViews, - deepEmbedViews, - labelViews, - feedGenViews, - listViews, - ] = await Promise.all([ - this.getPostViews(nestedPostUris, requester), - this.getActorViews(nestedDids, requester, { skipLabels: true }), - this.embedsForPosts(nestedPostUris, requester, _depth + 1), - this.services.label.getLabelsForSubjects([ - ...nestedPostUris, - ...nestedDids, - ]), - this.getFeedGeneratorViews(nestedFeedGenUris, requester), - this.services.graph.getListViews(nestedListUris, requester), + precomputed?: { + actors?: ActorInfoMap + posts?: PostInfoMap + embeds?: PostEmbedViews + labels?: Labels + }, + ): Promise { + const uris = dedupeStrs(postUris) + const dids = dedupeStrs(postUris.map((uri) => new AtUri(uri).hostname)) + + const [actors, posts, labels] = await Promise.all([ + precomputed?.actors ?? + this.getActorInfos(dids, requester, { skipLabels: true }), + precomputed?.posts ?? this.getPostInfos(uris, requester), + precomputed?.labels ?? + this.services.label.getLabelsForSubjects([...uris, ...dids]), ]) - let embeds = images.reduce((acc, cur) => { - const embed = (acc[cur.postUri] ??= { - $type: 'app.bsky.embed.images#view', - images: [], - }) - if (!isViewImages(embed)) return acc - embed.images.push({ - thumb: this.imgUriBuilder.getCommonSignedUri( - 'feed_thumbnail', - cur.imageCid, - ), - fullsize: this.imgUriBuilder.getCommonSignedUri( - 'feed_fullsize', - cur.imageCid, - ), - alt: cur.alt, - }) - return acc - }, {} as FeedEmbeds) - embeds = externals.reduce((acc, cur) => { - if (!acc[cur.postUri]) { - acc[cur.postUri] = { - $type: 'app.bsky.embed.external#view', - external: { - uri: cur.uri, - title: cur.title, - description: cur.description, - thumb: cur.thumbCid - ? this.imgUriBuilder.getCommonSignedUri( - 'feed_thumbnail', - cur.thumbCid, - ) - : undefined, - }, - } - } - return acc - }, embeds) - embeds = records.reduce((acc, cur) => { - const collection = new AtUri(cur.uri).collection - let recordEmbed: RecordEmbedView - if (collection === ids.AppBskyFeedGenerator && feedGenViews[cur.uri]) { - recordEmbed = { - record: { - $type: 'app.bsky.feed.defs#generatorView', - ...this.views.formatFeedGeneratorView( - feedGenViews[cur.uri], - actorViews, - labelViews, - ), - }, - } - } else if (collection === ids.AppBskyGraphList && listViews[cur.uri]) { - recordEmbed = { - record: { - $type: 'app.bsky.graph.defs#listView', - ...this.services.graph.formatListView( - listViews[cur.uri], - actorViews, - ), - }, - } - } else if (collection === ids.AppBskyFeedPost && postViews[cur.uri]) { - const formatted = this.views.formatPostView( - cur.uri, - actorViews, - postViews, - deepEmbedViews, - labelViews, - ) - let deepEmbeds: ViewRecord['embeds'] | undefined - if (_depth < 1) { - // Omit field entirely when too deep: e.g. don't include it on the embeds within a record embed. - // Otherwise list any embeds that appear within the record. A consumer may discover an embed - // within the raw record, then look within this array to find the presented view of it. - deepEmbeds = formatted?.embed ? [formatted.embed] : [] - } - recordEmbed = { - record: getRecordEmbedView(cur.uri, formatted, deepEmbeds), - } - } else { - recordEmbed = { - record: { - $type: 'app.bsky.embed.record#viewNotFound', - uri: cur.uri, - }, - } - } - if (acc[cur.postUri]) { - const mediaEmbed = acc[cur.postUri] - if (isViewImages(mediaEmbed) || isViewExternal(mediaEmbed)) { - acc[cur.postUri] = { - $type: 'app.bsky.embed.recordWithMedia#view', - record: recordEmbed, - media: mediaEmbed, - } - } - } else { - acc[cur.postUri] = { - $type: 'app.bsky.embed.record#view', - ...recordEmbed, - } + const embeds = + precomputed?.embeds ?? (await this.embedsForPosts(posts, requester)) + + return uris.reduce((acc, cur) => { + const view = this.views.formatPostView(cur, actors, posts, embeds, labels) + if (view) { + acc[cur] = view } return acc - }, embeds) - return embeds + }, {} as PostViews) } async hydrateFeed( @@ -451,14 +311,14 @@ export class FeedService { actorDids.add(new AtUri(item.replyRoot).hostname) } } - const [actors, posts, embeds, labels] = await Promise.all([ - this.getActorViews(Array.from(actorDids), requester, { + const [actors, posts, labels] = await Promise.all([ + this.getActorInfos(Array.from(actorDids), requester, { skipLabels: true, }), - this.getPostViews(Array.from(postUris), requester), - this.embedsForPosts(Array.from(postUris), requester), + this.getPostInfos(Array.from(postUris), requester), this.services.label.getLabelsForSubjects([...postUris, ...actorDids]), ]) + const embeds = await this.embedsForPosts(posts, requester) return this.views.formatFeed( items, @@ -469,6 +329,122 @@ export class FeedService { usePostViewUnion, ) } + + async embedsForPosts(postInfos: PostInfoMap, requester: string, depth = 0) { + const postMap = postRecordsFromInfos(postInfos) + const posts = Object.values(postMap) + if (posts.length < 1) { + return {} + } + const recordEmbedViews = + depth > 1 ? {} : await this.nestedRecordViews(posts, requester, depth) + + const postEmbedViews: PostEmbedViews = {} + for (const [uri, post] of Object.entries(postMap)) { + if (!post.embed) continue + if (isEmbedImages(post.embed)) { + postEmbedViews[uri] = this.views.imagesEmbedView(post.embed) + } else if (isEmbedExternal(post.embed)) { + postEmbedViews[uri] = this.views.externalEmbedView(post.embed) + } else if (isEmbedRecord(post.embed)) { + if (!recordEmbedViews[post.embed.record.uri]) continue + postEmbedViews[uri] = { + $type: 'app.bsky.embed.record#view', + record: recordEmbedViews[post.embed.record.uri], + } + } else if (isEmbedRecordWithMedia(post.embed)) { + const embedRecordView = recordEmbedViews[post.embed.record.record.uri] + if (!embedRecordView) continue + const formatted = this.views.getRecordWithMediaEmbedView( + post.embed, + embedRecordView, + ) + if (formatted) { + postEmbedViews[uri] = formatted + } + } + } + return postEmbedViews + } + + async nestedRecordViews( + posts: PostRecord[], + requester: string, + depth: number, + ): Promise { + const nestedUris = nestedRecordUris(posts) + if (nestedUris.length < 1) return {} + const nestedPostUris: string[] = [] + const nestedFeedGenUris: string[] = [] + const nestedListUris: string[] = [] + const nestedDidsSet = new Set() + for (const uri of nestedUris) { + const parsed = new AtUri(uri) + nestedDidsSet.add(parsed.hostname) + if (parsed.collection === ids.AppBskyFeedPost) { + nestedPostUris.push(uri) + } else if (parsed.collection === ids.AppBskyFeedGenerator) { + nestedFeedGenUris.push(uri) + } else if (parsed.collection === ids.AppBskyGraphList) { + nestedListUris.push(uri) + } + } + const nestedDids = [...nestedDidsSet] + const [postInfos, actorInfos, labelViews, feedGenInfos, listViews] = + await Promise.all([ + this.getPostInfos(nestedPostUris, requester), + this.getActorInfos(nestedDids, requester, { skipLabels: true }), + this.services.label.getLabelsForSubjects([ + ...nestedPostUris, + ...nestedDids, + ]), + this.getFeedGeneratorInfos(nestedFeedGenUris, requester), + this.services.graph.getListViews(nestedListUris, requester), + ]) + const deepEmbedViews = await this.embedsForPosts( + postInfos, + requester, + depth + 1, + ) + const recordEmbedViews: RecordEmbedViewRecordMap = {} + for (const uri of nestedUris) { + const collection = new AtUri(uri).collection + if (collection === ids.AppBskyFeedGenerator && feedGenInfos[uri]) { + recordEmbedViews[uri] = { + $type: 'app.bsky.feed.defs#generatorView', + ...this.views.formatFeedGeneratorView( + feedGenInfos[uri], + actorInfos, + labelViews, + ), + } + } else if (collection === ids.AppBskyGraphList && listViews[uri]) { + recordEmbedViews[uri] = { + $type: 'app.bsky.graph.defs#listView', + ...this.services.graph.formatListView(listViews[uri], actorInfos), + } + } else if (collection === ids.AppBskyFeedPost && postInfos[uri]) { + const formatted = this.views.formatPostView( + uri, + actorInfos, + postInfos, + deepEmbedViews, + labelViews, + ) + recordEmbedViews[uri] = this.views.getRecordEmbedView( + uri, + formatted, + depth > 0, + ) + } else { + recordEmbedViews[uri] = { + $type: 'app.bsky.embed.record#viewNotFound', + uri, + } + } + } + return recordEmbedViews + } } function truncateUtf8(str: string | null | undefined, length: number) { @@ -483,31 +459,30 @@ function truncateUtf8(str: string | null | undefined, length: number) { return str } -function getRecordEmbedView( - uri: string, - post?: PostView, - embeds?: ViewRecord['embeds'], -): (ViewRecord | ViewNotFound | ViewBlocked) & { $type: string } { - if (!post) { - return { - $type: 'app.bsky.embed.record#viewNotFound', - uri, +const postRecordsFromInfos = ( + infos: PostInfoMap, +): { [uri: string]: PostRecord } => { + const records: { [uri: string]: PostRecord } = {} + for (const [uri, info] of Object.entries(infos)) { + const record = cborToLexRecord(info.recordBytes) + if (isPostRecord(record)) { + records[uri] = record } } - if (post.author.viewer?.blocking || post.author.viewer?.blockedBy) { - return { - $type: 'app.bsky.embed.record#viewBlocked', - uri, + return records +} + +const nestedRecordUris = (posts: PostRecord[]): string[] => { + const uris: string[] = [] + for (const post of posts) { + if (!post.embed) continue + if (isEmbedRecord(post.embed)) { + uris.push(post.embed.record.uri) + } else if (isEmbedRecordWithMedia(post.embed)) { + uris.push(post.embed.record.record.uri) + } else { + continue } } - return { - $type: 'app.bsky.embed.record#viewRecord', - uri: post.uri, - cid: post.cid, - author: post.author, - value: post.record, - labels: post.labels, - indexedAt: post.indexedAt, - embeds, - } + return uris } diff --git a/packages/pds/src/app-view/services/feed/types.ts b/packages/pds/src/app-view/services/feed/types.ts index 3e76c46bee9..d81f856a86e 100644 --- a/packages/pds/src/app-view/services/feed/types.ts +++ b/packages/pds/src/app-view/services/feed/types.ts @@ -1,19 +1,34 @@ -import { View as ViewImages } from '../../../lexicon/types/app/bsky/embed/images' -import { View as ViewExternal } from '../../../lexicon/types/app/bsky/embed/external' -import { View as ViewRecord } from '../../../lexicon/types/app/bsky/embed/record' -import { View as ViewRecordWithMedia } from '../../../lexicon/types/app/bsky/embed/recordWithMedia' +import { View as ImagesEmbedView } from '../../../lexicon/types/app/bsky/embed/images' +import { View as ExternalEmbedView } from '../../../lexicon/types/app/bsky/embed/external' +import { + ViewBlocked, + ViewNotFound, + ViewRecord, + View as RecordEmbedView, +} from '../../../lexicon/types/app/bsky/embed/record' +import { View as RecordWithMediaEmbedView } from '../../../lexicon/types/app/bsky/embed/recordWithMedia' import { BlockedPost, + GeneratorView, NotFoundPost, PostView, } from '../../../lexicon/types/app/bsky/feed/defs' import { Label } from '../../../lexicon/types/com/atproto/label/defs' import { FeedGenerator } from '../../db/tables/feed-generator' +import { ListView } from '../../../lexicon/types/app/bsky/graph/defs' -export type FeedEmbeds = { - [uri: string]: ViewImages | ViewExternal | ViewRecord | ViewRecordWithMedia +export type PostEmbedViews = { + [uri: string]: PostEmbedView } +export type PostEmbedView = + | ImagesEmbedView + | ExternalEmbedView + | RecordEmbedView + | RecordWithMediaEmbedView + +export type PostViews = { [uri: string]: PostView } + export type PostInfo = { uri: string cid: string @@ -29,7 +44,7 @@ export type PostInfo = { export type PostInfoMap = { [uri: string]: PostInfo } -export type ActorView = { +export type ActorInfo = { did: string handle: string displayName?: string @@ -43,7 +58,7 @@ export type ActorView = { } labels?: Label[] } -export type ActorViewMap = { [did: string]: ActorView } +export type ActorInfoMap = { [did: string]: ActorInfo } export type FeedGenInfo = FeedGenerator & { likeCount: number @@ -67,3 +82,12 @@ export type FeedRow = { } export type MaybePostView = PostView | NotFoundPost | BlockedPost + +export type RecordEmbedViewRecord = + | ViewRecord + | ViewNotFound + | ViewBlocked + | GeneratorView + | ListView + +export type RecordEmbedViewRecordMap = { [uri: string]: RecordEmbedViewRecord } diff --git a/packages/pds/src/app-view/services/feed/views.ts b/packages/pds/src/app-view/services/feed/views.ts index 1e4e7b83e18..21ed899a8c6 100644 --- a/packages/pds/src/app-view/services/feed/views.ts +++ b/packages/pds/src/app-view/services/feed/views.ts @@ -6,12 +6,29 @@ import { PostView, } from '../../../lexicon/types/app/bsky/feed/defs' import { - ActorViewMap, - FeedEmbeds, + Main as EmbedImages, + isMain as isEmbedImages, + View as EmbedImagesView, +} from '../../../lexicon/types/app/bsky/embed/images' +import { + Main as EmbedExternal, + isMain as isEmbedExternal, + View as EmbedExternalView, +} from '../../../lexicon/types/app/bsky/embed/external' +import { Main as EmbedRecordWithMedia } from '../../../lexicon/types/app/bsky/embed/recordWithMedia' +import { + ViewBlocked, + ViewNotFound, + ViewRecord, +} from '../../../lexicon/types/app/bsky/embed/record' +import { + ActorInfoMap, + PostEmbedViews, FeedGenInfo, FeedRow, MaybePostView, PostInfoMap, + RecordEmbedViewRecord, } from './types' import { Labels } from '../label' import { ProfileView } from '../../../lexicon/types/app/bsky/actor/defs' @@ -60,9 +77,9 @@ export class FeedViews { formatFeed( items: FeedRow[], - actors: ActorViewMap, + actors: ActorInfoMap, posts: PostInfoMap, - embeds: FeedEmbeds, + embeds: PostEmbedViews, labels: Labels, usePostViewUnion?: boolean, ): FeedViewPost[] { @@ -127,9 +144,9 @@ export class FeedViews { formatPostView( uri: string, - actors: ActorViewMap, + actors: ActorInfoMap, posts: PostInfoMap, - embeds: FeedEmbeds, + embeds: PostEmbedViews, labels: Labels, ): PostView | undefined { const post = posts[uri] @@ -158,9 +175,9 @@ export class FeedViews { formatMaybePostView( uri: string, - actors: ActorViewMap, + actors: ActorInfoMap, posts: PostInfoMap, - embeds: FeedEmbeds, + embeds: PostEmbedViews, labels: Labels, usePostViewUnion?: boolean, ): MaybePostView | undefined { @@ -194,4 +211,87 @@ export class FeedViews { notFound: true as const, } } + + imagesEmbedView(embed: EmbedImages) { + const imgViews = embed.images.map((img) => ({ + thumb: this.imgUriBuilder.getCommonSignedUri( + 'feed_thumbnail', + img.image.ref, + ), + fullsize: this.imgUriBuilder.getCommonSignedUri( + 'feed_fullsize', + img.image.ref, + ), + alt: img.alt, + })) + return { + $type: 'app.bsky.embed.images#view', + images: imgViews, + } + } + + externalEmbedView(embed: EmbedExternal) { + const { uri, title, description, thumb } = embed.external + return { + $type: 'app.bsky.embed.external#view', + external: { + uri, + title, + description, + thumb: thumb + ? this.imgUriBuilder.getCommonSignedUri('feed_thumbnail', thumb.ref) + : undefined, + }, + } + } + + getRecordEmbedView( + uri: string, + post?: PostView, + omitEmbeds = false, + ): (ViewRecord | ViewNotFound | ViewBlocked) & { $type: string } { + if (!post) { + return { + $type: 'app.bsky.embed.record#viewNotFound', + uri, + } + } + if (post.author.viewer?.blocking || post.author.viewer?.blockedBy) { + return { + $type: 'app.bsky.embed.record#viewBlocked', + uri, + } + } + return { + $type: 'app.bsky.embed.record#viewRecord', + uri: post.uri, + cid: post.cid, + author: post.author, + value: post.record, + labels: post.labels, + indexedAt: post.indexedAt, + embeds: omitEmbeds ? undefined : post.embed ? [post.embed] : [], + } + } + + getRecordWithMediaEmbedView( + embed: EmbedRecordWithMedia, + embedRecordView: RecordEmbedViewRecord, + ) { + let mediaEmbed: EmbedImagesView | EmbedExternalView + if (isEmbedImages(embed.media)) { + mediaEmbed = this.imagesEmbedView(embed.media) + } else if (isEmbedExternal(embed.media)) { + mediaEmbed = this.externalEmbedView(embed.media) + } else { + return + } + return { + $type: 'app.bsky.embed.recordWithMedia#view', + record: { + record: embedRecordView, + }, + media: mediaEmbed, + } + } } diff --git a/packages/pds/tests/proxied/procedures.test.ts b/packages/pds/tests/proxied/procedures.test.ts index 4fdf2482dba..4df2b92356a 100644 --- a/packages/pds/tests/proxied/procedures.test.ts +++ b/packages/pds/tests/proxied/procedures.test.ts @@ -108,6 +108,7 @@ describe('proxies appview procedures', () => { encoding: 'application/json', }, ) + await network.processAll() // check const { data: result1 } = await agent.api.app.bsky.graph.getListMutes( {}, From 7637fdbf10eb2bdfe916e24236de7b5c363f659f Mon Sep 17 00:00:00 2001 From: devin ivy Date: Mon, 3 Jul 2023 09:48:27 -0400 Subject: [PATCH 010/237] backpressure on bsky backfill indexing (#1268) * backpressure on bsky backfill indexing * skip image resolution for text labeler * increase background queue concurrency for backfill * tidy --- packages/bsky/src/background.ts | 2 +- packages/bsky/src/config.ts | 12 ++++++++++++ packages/bsky/src/index.ts | 7 ++++++- packages/bsky/src/labeler/keyword.ts | 10 +++++++++- packages/bsky/src/subscription/repo.ts | 10 +++++++--- packages/bsky/src/subscription/util.ts | 6 +++++- 6 files changed, 40 insertions(+), 7 deletions(-) diff --git a/packages/bsky/src/background.ts b/packages/bsky/src/background.ts index a66ecf887b8..dbf47eb8868 100644 --- a/packages/bsky/src/background.ts +++ b/packages/bsky/src/background.ts @@ -5,7 +5,7 @@ import { dbLogger } from './logger' // A simple queue for in-process, out-of-band/backgrounded work export class BackgroundQueue { - queue = new PQueue({ concurrency: 10 }) + queue = new PQueue({ concurrency: 20 }) destroyed = false constructor(public db: Database) {} diff --git a/packages/bsky/src/config.ts b/packages/bsky/src/config.ts index de88cbe98a7..6d14771731b 100644 --- a/packages/bsky/src/config.ts +++ b/packages/bsky/src/config.ts @@ -23,6 +23,7 @@ export interface ServerConfigValues { hiveApiKey?: string adminPassword: string labelerKeywords: Record + indexerConcurrency?: number } export class ServerConfig { @@ -61,6 +62,7 @@ export class ServerConfig { const adminPassword = process.env.ADMIN_PASSWORD || 'admin' const labelerDid = process.env.LABELER_DID || 'did:example:labeler' const hiveApiKey = process.env.HIVE_API_KEY || undefined + const indexerConcurrency = maybeParseInt(process.env.INDEXER_CONCURRENCY) const labelerKeywords = {} return new ServerConfig({ version, @@ -83,6 +85,7 @@ export class ServerConfig { hiveApiKey, adminPassword, labelerKeywords, + indexerConcurrency, ...stripUndefineds(overrides ?? {}), }) } @@ -183,6 +186,10 @@ export class ServerConfig { get adminPassword() { return this.cfg.adminPassword } + + get indexerConcurrency() { + return this.cfg.indexerConcurrency + } } function stripUndefineds( @@ -196,3 +203,8 @@ function stripUndefineds( }) return result } + +function maybeParseInt(str) { + const parsed = parseInt(str) + return isNaN(parsed) ? undefined : parsed +} diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 708f9e463d4..771b0ef27c3 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -157,7 +157,12 @@ export class BskyAppView { app.use(error.handler) const sub = config.repoProvider - ? new RepoSubscription(ctx, config.repoProvider, config.repoSubLockId) + ? new RepoSubscription( + ctx, + config.repoProvider, + config.repoSubLockId, + config.indexerConcurrency, + ) : undefined return new BskyAppView({ ctx, app, sub }) diff --git a/packages/bsky/src/labeler/keyword.ts b/packages/bsky/src/labeler/keyword.ts index d4d0e7735ac..b3fd745eaf1 100644 --- a/packages/bsky/src/labeler/keyword.ts +++ b/packages/bsky/src/labeler/keyword.ts @@ -1,9 +1,10 @@ import Database from '../db' import { Labeler } from './base' -import { keywordLabeling } from './util' +import { getFieldsFromRecord, keywordLabeling } from './util' import { IdResolver } from '@atproto/identity' import { ServerConfig } from '../config' import { BackgroundQueue } from '../background' +import { AtUri } from '@atproto/uri' export class KeywordLabeler extends Labeler { keywords: Record @@ -20,6 +21,13 @@ export class KeywordLabeler extends Labeler { this.keywords = ctx.cfg.labelerKeywords } + async labelRecord(_uri: AtUri, obj: unknown): Promise { + // skip image resolution + const { text } = getFieldsFromRecord(obj) + const txtLabels = await this.labelText(text.join(' ')) + return txtLabels + } + async labelText(text: string): Promise { return keywordLabeling(this.keywords, text) } diff --git a/packages/bsky/src/subscription/repo.ts b/packages/bsky/src/subscription/repo.ts index 174bd283d0f..2f4047805d1 100644 --- a/packages/bsky/src/subscription/repo.ts +++ b/packages/bsky/src/subscription/repo.ts @@ -25,7 +25,7 @@ export const REPO_SUB_ID = 1000 export class RepoSubscription { leader = new Leader(this.subLockId, this.ctx.db) - repoQueue = new PartitionedQueue() + repoQueue: PartitionedQueue cursorQueue = new LatestQueue() consecutive = new ConsecutiveList() destroyed = false @@ -34,7 +34,10 @@ export class RepoSubscription { public ctx: AppContext, public service: string, public subLockId = REPO_SUB_ID, - ) {} + public concurrency = Infinity, + ) { + this.repoQueue = new PartitionedQueue({ concurrency }) + } async run() { while (!this.destroyed) { @@ -81,6 +84,7 @@ export class RepoSubscription { ) }) }) + await this.repoQueue.main.onEmpty() // backpressure } }) if (ran && !this.destroyed) { @@ -107,7 +111,7 @@ export class RepoSubscription { async resume() { this.destroyed = false - this.repoQueue = new PartitionedQueue() + this.repoQueue = new PartitionedQueue({ concurrency: this.concurrency }) this.cursorQueue = new LatestQueue() this.consecutive = new ConsecutiveList() await this.run() diff --git a/packages/bsky/src/subscription/util.ts b/packages/bsky/src/subscription/util.ts index 70f3ec74905..0bd6b862176 100644 --- a/packages/bsky/src/subscription/util.ts +++ b/packages/bsky/src/subscription/util.ts @@ -3,9 +3,13 @@ import PQueue from 'p-queue' // A queue with arbitrarily many partitions, each processing work sequentially. // Partitions are created lazily and taken out of memory when they go idle. export class PartitionedQueue { - main = new PQueue({ concurrency: Infinity }) + main: PQueue partitions = new Map() + constructor(opts: { concurrency: number }) { + this.main = new PQueue({ concurrency: opts.concurrency }) + } + async add(partitionId: string, task: () => Promise) { if (this.main.isPaused) return return this.main.add(() => { From e7a0d27f1fef15d68a04be81cec449bfe3b1011f Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 3 Jul 2023 11:28:39 -0500 Subject: [PATCH 011/237] Proxy timeline skeleton construction (#1264) proxy timeline skeleton construction to appview --- .../bsky/unspecced/getTimelineSkeleton.json | 34 + packages/api/src/client/index.ts | 13 + packages/api/src/client/lexicons.ts | 49 + .../app/bsky/unspecced/getTimelineSkeleton.ts | 45 + .../getPopularFeedGenerators.ts} | 6 +- .../app/bsky/unspecced/getTimelineSkeleton.ts | 80 ++ packages/bsky/src/api/index.ts | 6 +- packages/bsky/src/lexicon/index.ts | 11 + packages/bsky/src/lexicon/lexicons.ts | 49 + .../app/bsky/unspecced/getTimelineSkeleton.ts | 46 + packages/dev-env/src/pds.ts | 2 +- packages/dev-env/src/types.ts | 3 +- .../src/app-view/api/app/bsky/feed/getFeed.ts | 18 +- .../app-view/api/app/bsky/feed/getTimeline.ts | 32 +- packages/pds/src/lexicon/index.ts | 11 + packages/pds/src/lexicon/lexicons.ts | 49 + .../app/bsky/unspecced/getTimelineSkeleton.ts | 46 + .../timeline-skeleton.test.ts.snap | 1227 +++++++++++++++++ .../tests/proxied/timeline-skeleton.test.ts | 59 + packages/pds/tests/proxied/views.test.ts | 4 +- 20 files changed, 1768 insertions(+), 22 deletions(-) create mode 100644 lexicons/app/bsky/unspecced/getTimelineSkeleton.json create mode 100644 packages/api/src/client/types/app/bsky/unspecced/getTimelineSkeleton.ts rename packages/bsky/src/api/app/bsky/{unspecced.ts => unspecced/getPopularFeedGenerators.ts} (92%) create mode 100644 packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts create mode 100644 packages/bsky/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts create mode 100644 packages/pds/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts create mode 100644 packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap create mode 100644 packages/pds/tests/proxied/timeline-skeleton.test.ts diff --git a/lexicons/app/bsky/unspecced/getTimelineSkeleton.json b/lexicons/app/bsky/unspecced/getTimelineSkeleton.json new file mode 100644 index 00000000000..7ac6d7dbdec --- /dev/null +++ b/lexicons/app/bsky/unspecced/getTimelineSkeleton.json @@ -0,0 +1,34 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getTimelineSkeleton", + "defs": { + "main": { + "type": "query", + "description": "A skeleton of a timeline - UNSPECCED & WILL GO AWAY 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": "app.bsky.feed.defs#skeletonFeedPost"} + } + } + } + }, + "errors": [ + {"name": "UnknownFeed"} + ] + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index e74bee840e0..9e66af0441d 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -123,6 +123,7 @@ import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/up import * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet' 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' export * as ComAtprotoAdminDefs from './types/com/atproto/admin/defs' export * as ComAtprotoAdminDisableAccountInvites from './types/com/atproto/admin/disableAccountInvites' @@ -240,6 +241,7 @@ export * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/up export * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet' export * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' export * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators' +export * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton' export const COM_ATPROTO_ADMIN = { DefsTakedown: 'com.atproto.admin.defs#takedown', @@ -2047,4 +2049,15 @@ export class UnspeccedNS { throw AppBskyUnspeccedGetPopularFeedGenerators.toKnownErr(e) }) } + + getTimelineSkeleton( + params?: AppBskyUnspeccedGetTimelineSkeleton.QueryParams, + opts?: AppBskyUnspeccedGetTimelineSkeleton.CallOptions, + ): Promise { + return this._service.xrpc + .call('app.bsky.unspecced.getTimelineSkeleton', params, undefined, opts) + .catch((e) => { + throw AppBskyUnspeccedGetTimelineSkeleton.toKnownErr(e) + }) + } } diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index e7a5195328e..f2adcb93c69 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -6304,6 +6304,54 @@ export const schemaDict = { }, }, }, + AppBskyUnspeccedGetTimelineSkeleton: { + lexicon: 1, + id: 'app.bsky.unspecced.getTimelineSkeleton', + defs: { + main: { + type: 'query', + description: 'A skeleton of a timeline', + 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', + }, + ], + }, + }, + }, } export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[] export const lexicons: Lexicons = new Lexicons(schemas) @@ -6432,4 +6480,5 @@ export const ids = { AppBskyUnspeccedGetPopular: 'app.bsky.unspecced.getPopular', AppBskyUnspeccedGetPopularFeedGenerators: 'app.bsky.unspecced.getPopularFeedGenerators', + AppBskyUnspeccedGetTimelineSkeleton: 'app.bsky.unspecced.getTimelineSkeleton', } diff --git a/packages/api/src/client/types/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/api/src/client/types/app/bsky/unspecced/getTimelineSkeleton.ts new file mode 100644 index 00000000000..de9ba9c0ae9 --- /dev/null +++ b/packages/api/src/client/types/app/bsky/unspecced/getTimelineSkeleton.ts @@ -0,0 +1,45 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as AppBskyFeedDefs from '../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 interface CallOptions { + headers?: Headers +} + +export interface Response { + success: boolean + headers: Headers + data: OutputSchema +} + +export class UnknownFeedError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message) + } +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + if (e.error === 'UnknownFeed') return new UnknownFeedError(e) + } + return e +} diff --git a/packages/bsky/src/api/app/bsky/unspecced.ts b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts similarity index 92% rename from packages/bsky/src/api/app/bsky/unspecced.ts rename to packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index 1f4e4bf7f41..303fed257cb 100644 --- a/packages/bsky/src/api/app/bsky/unspecced.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -1,6 +1,6 @@ -import { Server } from '../../../lexicon' -import AppContext from '../../../context' -import { countAll } from '../../../db/util' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { countAll } from '../../../../db/util' // THIS IS A TEMPORARY UNSPECCED ROUTE export default function (server: Server, ctx: AppContext) { diff --git a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts new file mode 100644 index 00000000000..0684ce91144 --- /dev/null +++ b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts @@ -0,0 +1,80 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { FeedKeyset, getFeedDateThreshold } from '../util/feed' +import { paginate } from '../../../../db/pagination' + +// THIS IS A TEMPORARY UNSPECCED ROUTE +export default function (server: Server, ctx: AppContext) { + server.app.bsky.unspecced.getTimelineSkeleton({ + auth: ctx.authVerifier, + handler: async ({ auth, params }) => { + const { limit, cursor } = params + const viewer = auth.credentials.did + + const db = ctx.db.db + const { ref } = db.dynamic + + const feedService = ctx.services.feed(ctx.db) + const graphService = ctx.services.graph(ctx.db) + + const followingIdsSubquery = db + .selectFrom('follow') + .select('follow.subjectDid') + .where('follow.creator', '=', viewer) + + const keyset = new FeedKeyset( + ref('feed_item.sortAt'), + ref('feed_item.cid'), + ) + const sortFrom = keyset.unpack(cursor)?.primary + + let feedItemsQb = feedService + .selectFeedItemQb() + .where((qb) => + qb + .where('originatorDid', '=', viewer) + .orWhere('originatorDid', 'in', followingIdsSubquery), + ) + .where((qb) => + // Hide posts and reposts of or by muted actors + graphService.whereNotMuted(qb, viewer, [ + ref('post.creator'), + ref('originatorDid'), + ]), + ) + .whereNotExists( + graphService.blockQb(viewer, [ + ref('post.creator'), + ref('originatorDid'), + ]), + ) + .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom)) + + feedItemsQb = paginate(feedItemsQb, { + limit, + cursor, + keyset, + tryIndex: true, + }) + + const feedItems = await feedItemsQb.execute() + const feed = feedItems.map((item) => ({ + post: item.postUri, + reason: + item.uri === item.postUri + ? undefined + : { + $type: 'app.bsky.feed.defs#skeletonReasonRepost', + repost: item.uri, + }, + })) + return { + encoding: 'application/json', + body: { + cursor: keyset.packFromResult(feedItems), + feed, + }, + } + }, + }) +} diff --git a/packages/bsky/src/api/index.ts b/packages/bsky/src/api/index.ts index c56765c8d56..691eae882d1 100644 --- a/packages/bsky/src/api/index.ts +++ b/packages/bsky/src/api/index.ts @@ -30,7 +30,8 @@ import getSuggestions from './app/bsky/actor/getSuggestions' import getUnreadCount from './app/bsky/notification/getUnreadCount' import listNotifications from './app/bsky/notification/listNotifications' import updateSeen from './app/bsky/notification/updateSeen' -import unspecced from './app/bsky/unspecced' +import getPopularFeedGenerators from './app/bsky/unspecced/getPopularFeedGenerators' +import getTimelineSkeleton from './app/bsky/unspecced/getTimelineSkeleton' import createReport from './com/atproto/moderation/createReport' import resolveModerationReports from './com/atproto/admin/resolveModerationReports' import reverseModerationAction from './com/atproto/admin/reverseModerationAction' @@ -81,7 +82,8 @@ export default function (server: Server, ctx: AppContext) { getUnreadCount(server, ctx) listNotifications(server, ctx) updateSeen(server, ctx) - unspecced(server, ctx) + getPopularFeedGenerators(server, ctx) + getTimelineSkeleton(server, ctx) // com.atproto createReport(server, ctx) resolveModerationReports(server, ctx) diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 548011fc331..982ed642924 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -103,6 +103,7 @@ import * as AppBskyNotificationListNotifications from './types/app/bsky/notifica 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' export const COM_ATPROTO_ADMIN = { DefsTakedown: 'com.atproto.admin.defs#takedown', @@ -1048,6 +1049,16 @@ export class UnspeccedNS { const nsid = 'app.bsky.unspecced.getPopularFeedGenerators' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } + + getTimelineSkeleton( + cfg: ConfigOf< + AV, + AppBskyUnspeccedGetTimelineSkeleton.Handler> + >, + ) { + const nsid = 'app.bsky.unspecced.getTimelineSkeleton' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } } type ConfigOf = diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index e7a5195328e..f2adcb93c69 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -6304,6 +6304,54 @@ export const schemaDict = { }, }, }, + AppBskyUnspeccedGetTimelineSkeleton: { + lexicon: 1, + id: 'app.bsky.unspecced.getTimelineSkeleton', + defs: { + main: { + type: 'query', + description: 'A skeleton of a timeline', + 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', + }, + ], + }, + }, + }, } export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[] export const lexicons: Lexicons = new Lexicons(schemas) @@ -6432,4 +6480,5 @@ export const ids = { AppBskyUnspeccedGetPopular: 'app.bsky.unspecced.getPopular', AppBskyUnspeccedGetPopularFeedGenerators: 'app.bsky.unspecced.getPopularFeedGenerators', + AppBskyUnspeccedGetTimelineSkeleton: 'app.bsky.unspecced.getTimelineSkeleton', } diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts new file mode 100644 index 00000000000..1178a844a93 --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.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 '../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 Handler = (ctx: { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +}) => Promise | HandlerOutput diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index facb5ceed4b..6a818e4e235 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -77,7 +77,7 @@ export class TestPds { : pds.Database.memory() await db.migrateToLatestOrThrow() - if (config.bskyAppViewEndpoint) { + if (config.bskyAppViewEndpoint && !cfg.enableAppView) { // Disable communication to app view within pds MessageDispatcher.prototype.send = async () => {} } diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index ec088ec4806..408ce8cee4f 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -9,6 +9,7 @@ export type PlcConfig = { export type PdsConfig = Partial & { plcUrl: string migration?: string + enableAppView?: boolean } export type BskyConfig = Partial & { @@ -22,7 +23,7 @@ export type BskyConfig = Partial & { export type TestServerParams = { dbPostgresUrl: string dbPostgresSchema: string - pds: Partial + pds: Partial plc: Partial bsky: Partial } diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts index ca1ea03c495..e521f4fda34 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts @@ -139,9 +139,17 @@ async function skeletonFromFeedGen( throw err } + return filterMutesAndBlocks(ctx, skeleton, params.limit, requester) +} + +export async function filterMutesAndBlocks( + ctx: AppContext, + skeleton: SkeletonOutput, + limit: number, + requester: string, +) { const { feed: skeletonFeed, ...rest } = skeleton - // Hydrate feed skeleton const { ref } = ctx.db.db.dynamic const feedService = ctx.services.appView.feed(ctx.db) const graphService = ctx.services.appView.graph(ctx.db) @@ -168,7 +176,7 @@ async function skeletonFromFeedGen( .execute() : [] - const orderedItems = getOrderedFeedItems(skeletonFeed, feedItems, params) + const orderedItems = getOrderedFeedItems(skeletonFeed, feedItems, limit) return { ...rest, feedItems: orderedItems, @@ -185,15 +193,15 @@ function getSkeleFeedItemUri(item: SkeletonFeedPost) { function getOrderedFeedItems( skeletonItems: SkeletonFeedPost[], feedItems: FeedRow[], - params: GetFeedParams, + limit: number, ) { const SKIP = [] const feedItemsByUri = feedItems.reduce((acc, item) => { return Object.assign(acc, { [item.uri]: item }) }, {} as Record) // enforce limit param in the case that the feedgen does not - if (skeletonItems.length > params.limit) { - skeletonItems = skeletonItems.slice(0, params.limit) + if (skeletonItems.length > limit) { + skeletonItems = skeletonItems.slice(0, limit) } return skeletonItems.flatMap((item) => { const uri = getSkeleFeedItemUri(item) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts b/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts index 8da47aab4af..c6b0bd9e7a4 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts @@ -4,31 +4,45 @@ import { FeedAlgorithm, FeedKeyset, getFeedDateThreshold } from '../util/feed' import { paginate } from '../../../../../db/pagination' import AppContext from '../../../../../context' import { FeedRow } from '../../../../services/feed' +import { filterMutesAndBlocks } from './getFeed' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getTimeline({ auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did + const { algorithm, limit, cursor } = params + if (algorithm && algorithm !== FeedAlgorithm.ReverseChronological) { + throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`) + } + if (ctx.canProxy(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getTimeline( - params, - await ctx.serviceAuthHeaders(requester), + const res = + await ctx.appviewAgent.api.app.bsky.unspecced.getTimelineSkeleton( + params, + await ctx.serviceAuthHeaders(requester), + ) + const filtered = await filterMutesAndBlocks( + ctx, + res.data, + limit, + requester, ) + const hydrated = await ctx.services.appView + .feed(ctx.db) + .hydrateFeed(filtered.feedItems, requester) return { encoding: 'application/json', - body: res.data, + body: { + cursor: filtered.cursor, + feed: hydrated, + }, } } - const { algorithm, limit, cursor } = params const db = ctx.db.db const { ref } = db.dynamic - if (algorithm && algorithm !== FeedAlgorithm.ReverseChronological) { - throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`) - } - const accountService = ctx.services.account(ctx.db) const feedService = ctx.services.appView.feed(ctx.db) const graphService = ctx.services.appView.graph(ctx.db) diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 548011fc331..982ed642924 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -103,6 +103,7 @@ import * as AppBskyNotificationListNotifications from './types/app/bsky/notifica 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' export const COM_ATPROTO_ADMIN = { DefsTakedown: 'com.atproto.admin.defs#takedown', @@ -1048,6 +1049,16 @@ export class UnspeccedNS { const nsid = 'app.bsky.unspecced.getPopularFeedGenerators' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } + + getTimelineSkeleton( + cfg: ConfigOf< + AV, + AppBskyUnspeccedGetTimelineSkeleton.Handler> + >, + ) { + const nsid = 'app.bsky.unspecced.getTimelineSkeleton' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } } type ConfigOf = diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index e7a5195328e..f2adcb93c69 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -6304,6 +6304,54 @@ export const schemaDict = { }, }, }, + AppBskyUnspeccedGetTimelineSkeleton: { + lexicon: 1, + id: 'app.bsky.unspecced.getTimelineSkeleton', + defs: { + main: { + type: 'query', + description: 'A skeleton of a timeline', + 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', + }, + ], + }, + }, + }, } export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[] export const lexicons: Lexicons = new Lexicons(schemas) @@ -6432,4 +6480,5 @@ export const ids = { AppBskyUnspeccedGetPopular: 'app.bsky.unspecced.getPopular', AppBskyUnspeccedGetPopularFeedGenerators: 'app.bsky.unspecced.getPopularFeedGenerators', + AppBskyUnspeccedGetTimelineSkeleton: 'app.bsky.unspecced.getTimelineSkeleton', } diff --git a/packages/pds/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/pds/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts new file mode 100644 index 00000000000..1178a844a93 --- /dev/null +++ b/packages/pds/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.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 '../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 Handler = (ctx: { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +}) => Promise | HandlerOutput diff --git a/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap new file mode 100644 index 00000000000..3c6ca0ad290 --- /dev/null +++ b/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap @@ -0,0 +1,1227 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`proxies timeline skeleton timeline skeleton construction 1`] = ` +Object { + "cursor": "0000000000000::bafycid", + "feed": Array [ + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object {}, + }, + "reason": Object { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": Object { + "did": "user(1)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(1)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(1)", + "muted": false, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(1)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(1)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(1)", + "muted": false, + }, + }, + "cid": "cids(1)", + "embed": Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "did": "user(2)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(5)", + "following": "record(4)", + "muted": false, + }, + }, + "cid": "cids(2)", + "embeds": Array [ + Object { + "$type": "app.bsky.embed.recordWithMedia#view", + "media": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + }, + Object { + "alt": "tests/image/fixtures/key-alt.jpg", + "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", + "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", + }, + ], + }, + "record": Object { + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(3)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(8)", + "following": "record(7)", + "muted": false, + }, + }, + "cid": "cids(5)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(6)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "langs": Array [ + "en-US", + "i-klingon", + ], + "text": "bob back at it again!", + }, + }, + }, + }, + ], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(3)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia", + "media": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(3)", + }, + "size": 4114, + }, + }, + Object { + "alt": "tests/image/fixtures/key-alt.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(4)", + }, + "size": 12736, + }, + }, + ], + }, + "record": Object { + "record": Object { + "cid": "cids(5)", + "uri": "record(6)", + }, + }, + }, + "text": "hi im carol", + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(2)", + "uri": "record(3)", + }, + }, + "facets": Array [ + Object { + "features": Array [ + Object { + "$type": "app.bsky.richtext.facet#mention", + "did": "user(0)", + }, + ], + "index": Object { + "byteEnd": 18, + "byteStart": 0, + }, + }, + ], + "text": "@alice.bluesky.xyz is the best", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(2)", + "viewer": Object {}, + }, + "reason": Object { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": Object { + "did": "user(2)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(5)", + "following": "record(4)", + "muted": false, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(6)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(7)", + "uri": "record(10)", + }, + "root": Object { + "cid": "cids(0)", + "uri": "record(0)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(9)", + "viewer": Object {}, + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(3)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(8)", + "following": "record(7)", + "muted": false, + }, + }, + "cid": "cids(7)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(10)", + "val": "test-label", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(10)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(3)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(0)", + "uri": "record(0)", + }, + "root": Object { + "cid": "cids(0)", + "uri": "record(0)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(10)", + "viewer": Object {}, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object {}, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(2)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(5)", + "following": "record(4)", + "muted": false, + }, + }, + "cid": "cids(8)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(0)", + "uri": "record(0)", + }, + "root": Object { + "cid": "cids(0)", + "uri": "record(0)", + }, + }, + "text": "of course", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(11)", + "viewer": Object {}, + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object {}, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object {}, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(3)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(8)", + "following": "record(7)", + "muted": false, + }, + }, + "cid": "cids(7)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(10)", + "val": "test-label", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(10)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(3)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(0)", + "uri": "record(0)", + }, + "root": Object { + "cid": "cids(0)", + "uri": "record(0)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(10)", + "viewer": Object {}, + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object {}, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object {}, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(9)", + "embed": Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "did": "user(1)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(1)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(1)", + "muted": false, + }, + }, + "cid": "cids(1)", + "embeds": Array [ + Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "did": "user(2)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(5)", + "following": "record(4)", + "muted": false, + }, + }, + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(3)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia", + "media": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(3)", + }, + "size": 4114, + }, + }, + Object { + "alt": "tests/image/fixtures/key-alt.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(4)", + }, + "size": 12736, + }, + }, + ], + }, + "record": Object { + "record": Object { + "cid": "cids(5)", + "uri": "record(6)", + }, + }, + }, + "text": "hi im carol", + }, + }, + }, + ], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(2)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(2)", + "uri": "record(3)", + }, + }, + "facets": Array [ + Object { + "features": Array [ + Object { + "$type": "app.bsky.richtext.facet#mention", + "did": "user(0)", + }, + ], + "index": Object { + "byteEnd": 18, + "byteStart": 0, + }, + }, + ], + "text": "@alice.bluesky.xyz is the best", + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(9)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(12)", + "val": "test-label", + }, + ], + "likeCount": 2, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(1)", + "uri": "record(2)", + }, + }, + "text": "yoohoo label_me", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(12)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(3)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(8)", + "following": "record(7)", + "muted": false, + }, + }, + "cid": "cids(10)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000+00:00", + "text": "bobby boy here", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(13)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(1)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(1)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(1)", + "muted": false, + }, + }, + "cid": "cids(1)", + "embed": Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "did": "user(2)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(5)", + "following": "record(4)", + "muted": false, + }, + }, + "cid": "cids(2)", + "embeds": Array [ + Object { + "$type": "app.bsky.embed.recordWithMedia#view", + "media": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + }, + Object { + "alt": "tests/image/fixtures/key-alt.jpg", + "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", + "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", + }, + ], + }, + "record": Object { + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(3)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(8)", + "following": "record(7)", + "muted": false, + }, + }, + "cid": "cids(5)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(6)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "langs": Array [ + "en-US", + "i-klingon", + ], + "text": "bob back at it again!", + }, + }, + }, + }, + ], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(3)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia", + "media": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(3)", + }, + "size": 4114, + }, + }, + Object { + "alt": "tests/image/fixtures/key-alt.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(4)", + }, + "size": 12736, + }, + }, + ], + }, + "record": Object { + "record": Object { + "cid": "cids(5)", + "uri": "record(6)", + }, + }, + }, + "text": "hi im carol", + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(2)", + "uri": "record(3)", + }, + }, + "facets": Array [ + Object { + "features": Array [ + Object { + "$type": "app.bsky.richtext.facet#mention", + "did": "user(0)", + }, + ], + "index": Object { + "byteEnd": 18, + "byteStart": 0, + }, + }, + ], + "text": "@alice.bluesky.xyz is the best", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(2)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(1)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(1)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(1)", + "muted": false, + }, + }, + "cid": "cids(11)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "dan here!", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(14)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(2)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(5)", + "following": "record(4)", + "muted": false, + }, + }, + "cid": "cids(2)", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia#view", + "media": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + }, + Object { + "alt": "tests/image/fixtures/key-alt.jpg", + "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", + "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", + }, + ], + }, + "record": Object { + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(3)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(8)", + "following": "record(7)", + "muted": false, + }, + }, + "cid": "cids(5)", + "embeds": Array [], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(6)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "langs": Array [ + "en-US", + "i-klingon", + ], + "text": "bob back at it again!", + }, + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 2, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia", + "media": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(3)", + }, + "size": 4114, + }, + }, + Object { + "alt": "tests/image/fixtures/key-alt.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(4)", + }, + "size": 12736, + }, + }, + ], + }, + "record": Object { + "record": Object { + "cid": "cids(5)", + "uri": "record(6)", + }, + }, + }, + "text": "hi im carol", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(3)", + "viewer": Object { + "like": "record(15)", + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(3)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(8)", + "following": "record(7)", + "muted": false, + }, + }, + "cid": "cids(5)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "langs": Array [ + "en-US", + "i-klingon", + ], + "text": "bob back at it again!", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(6)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(12)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "hey there", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(16)", + "viewer": Object {}, + }, + }, + ], +} +`; diff --git a/packages/pds/tests/proxied/timeline-skeleton.test.ts b/packages/pds/tests/proxied/timeline-skeleton.test.ts new file mode 100644 index 00000000000..680dd3423d4 --- /dev/null +++ b/packages/pds/tests/proxied/timeline-skeleton.test.ts @@ -0,0 +1,59 @@ +import AtpAgent from '@atproto/api' +import { TestNetwork } from '@atproto/dev-env' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' +import { forSnapshot } from '../_util' + +describe('proxies timeline skeleton', () => { + let network: TestNetwork + let agent: AtpAgent + let sc: SeedClient + + let alice: string + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'proxy_timeline_skeleton', + pds: { + enableAppView: true, + }, + }) + agent = network.pds.getClient() + sc = new SeedClient(agent) + await basicSeed(sc) + await network.processAll() + alice = sc.dids.alice + }) + + afterAll(async () => { + await network.close() + }) + + it('timeline skeleton construction', async () => { + const res = await agent.api.app.bsky.feed.getTimeline( + {}, + { + headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, + }, + ) + + expect(forSnapshot(res.data)).toMatchSnapshot() + const pt1 = await agent.api.app.bsky.feed.getTimeline( + { + limit: 2, + }, + { + headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, + }, + ) + const pt2 = await agent.api.app.bsky.feed.getTimeline( + { + cursor: pt1.data.cursor, + }, + { + headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, + }, + ) + expect([...pt1.data.feed, ...pt2.data.feed]).toEqual(res.data.feed) + }) +}) diff --git a/packages/pds/tests/proxied/views.test.ts b/packages/pds/tests/proxied/views.test.ts index ff5336c3e5f..33166b9de78 100644 --- a/packages/pds/tests/proxied/views.test.ts +++ b/packages/pds/tests/proxied/views.test.ts @@ -257,13 +257,15 @@ describe('proxies view requests', () => { expect(forSnapshot(res.data)).toMatchSnapshot() }) - it('feed.getTimeline', async () => { + // @TODO re-enable when proxying is a full-proxy + it.skip('feed.getTimeline', async () => { const res = await agent.api.app.bsky.feed.getTimeline( {}, { headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, }, ) + expect(forSnapshot(res.data)).toMatchSnapshot() const pt1 = await agent.api.app.bsky.feed.getTimeline( { From 6695e6c9e404685c3711ce4bf27fa412f5a612d3 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 3 Jul 2023 12:38:33 -0500 Subject: [PATCH 012/237] Only pass through known params on timeline skeleton (#1270) only pass through own params --- packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts b/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts index c6b0bd9e7a4..1b3e6271b50 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts @@ -19,7 +19,7 @@ export default function (server: Server, ctx: AppContext) { if (ctx.canProxy(req)) { const res = await ctx.appviewAgent.api.app.bsky.unspecced.getTimelineSkeleton( - params, + { limit, cursor }, await ctx.serviceAuthHeaders(requester), ) const filtered = await filterMutesAndBlocks( From 9610ba061c39bdc6048bbf929fe4842a870b72e8 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 3 Jul 2023 13:19:56 -0500 Subject: [PATCH 013/237] Require headers on getRecord proxy (#1271) require headers on getRecord proxy --- packages/pds/src/api/com/atproto/repo/getRecord.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pds/src/api/com/atproto/repo/getRecord.ts b/packages/pds/src/api/com/atproto/repo/getRecord.ts index 163411360f6..020935eaffa 100644 --- a/packages/pds/src/api/com/atproto/repo/getRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/getRecord.ts @@ -4,7 +4,7 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { - server.com.atproto.repo.getRecord(async ({ params }) => { + server.com.atproto.repo.getRecord(async ({ req, params }) => { const { repo, collection, rkey, cid } = params const did = await ctx.services.account(ctx.db).getDidForActor(repo) @@ -26,7 +26,7 @@ export default function (server: Server, ctx: AppContext) { } } - if (ctx.cfg.bskyAppViewEndpoint) { + if (ctx.canProxy(req)) { const res = await ctx.appviewAgent.api.com.atproto.repo.getRecord(params) return { encoding: 'application/json', From c793ff9103c1348fbc80c223d16e1ae8bc997b0a Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 3 Jul 2023 14:14:49 -0500 Subject: [PATCH 014/237] Add boolean for enabling generic appview proxying (#1273) * add boolean config for enabling generic proxying * tweak * tweak cfg var name * tweak --- packages/dev-env/src/pds.ts | 7 ++++++- packages/dev-env/src/types.ts | 2 +- .../api/com/atproto/identity/resolveHandle.ts | 4 ++-- .../pds/src/api/com/atproto/repo/getRecord.ts | 2 +- .../app-view/api/app/bsky/actor/getProfile.ts | 2 +- .../app-view/api/app/bsky/actor/getProfiles.ts | 2 +- .../api/app/bsky/actor/getSuggestions.ts | 2 +- .../app-view/api/app/bsky/actor/searchActors.ts | 2 +- .../api/app/bsky/actor/searchActorsTypeahead.ts | 2 +- .../app-view/api/app/bsky/feed/getActorFeeds.ts | 2 +- .../app-view/api/app/bsky/feed/getAuthorFeed.ts | 2 +- .../src/app-view/api/app/bsky/feed/getFeed.ts | 2 +- .../api/app/bsky/feed/getFeedGenerator.ts | 2 +- .../api/app/bsky/feed/getFeedGenerators.ts | 2 +- .../src/app-view/api/app/bsky/feed/getLikes.ts | 2 +- .../app-view/api/app/bsky/feed/getPostThread.ts | 2 +- .../src/app-view/api/app/bsky/feed/getPosts.ts | 2 +- .../app-view/api/app/bsky/feed/getRepostedBy.ts | 2 +- .../app-view/api/app/bsky/feed/getTimeline.ts | 12 +++++++++++- .../src/app-view/api/app/bsky/graph/getBlocks.ts | 2 +- .../app-view/api/app/bsky/graph/getFollowers.ts | 2 +- .../app-view/api/app/bsky/graph/getFollows.ts | 2 +- .../src/app-view/api/app/bsky/graph/getList.ts | 2 +- .../app-view/api/app/bsky/graph/getListMutes.ts | 2 +- .../src/app-view/api/app/bsky/graph/getLists.ts | 2 +- .../src/app-view/api/app/bsky/graph/getMutes.ts | 2 +- .../src/app-view/api/app/bsky/graph/muteActor.ts | 2 +- .../app-view/api/app/bsky/graph/muteActorList.ts | 2 +- .../app-view/api/app/bsky/graph/unmuteActor.ts | 2 +- .../api/app/bsky/graph/unmuteActorList.ts | 2 +- .../api/app/bsky/notification/getUnreadCount.ts | 2 +- .../app/bsky/notification/listNotifications.ts | 2 +- .../api/app/bsky/notification/updateSeen.ts | 2 +- .../pds/src/app-view/api/app/bsky/unspecced.ts | 2 +- packages/pds/src/config.ts | 8 ++++++++ packages/pds/src/context.ts | 16 +++++++++++++++- packages/pds/tests/_util.ts | 1 + .../pds/tests/proxied/timeline-skeleton.test.ts | 3 ++- 38 files changed, 76 insertions(+), 37 deletions(-) diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 6a818e4e235..ee0c307f141 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -64,6 +64,7 @@ export class TestPds { labelerKeywords: { label_me: 'test-label', label_me_2: 'test-label-2' }, feedGenDid: 'did:example:feedGen', dbTxLockNonce: await randomStr(32, 'base32'), + bskyAppViewProxy: !!cfg.bskyAppViewEndpoint, ...cfg, }) @@ -77,7 +78,11 @@ export class TestPds { : pds.Database.memory() await db.migrateToLatestOrThrow() - if (config.bskyAppViewEndpoint && !cfg.enableAppView) { + if ( + config.bskyAppViewEndpoint && + config.bskyAppViewProxy && + !cfg.enableInProcessAppView + ) { // Disable communication to app view within pds MessageDispatcher.prototype.send = async () => {} } diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index 408ce8cee4f..829d7f7518e 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -9,7 +9,7 @@ export type PlcConfig = { export type PdsConfig = Partial & { plcUrl: string migration?: string - enableAppView?: boolean + enableInProcessAppView?: boolean } export type BskyConfig = Partial & { diff --git a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts index f2c222e665d..bfa47b47764 100644 --- a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts @@ -5,7 +5,7 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { - server.com.atproto.identity.resolveHandle(async ({ params }) => { + server.com.atproto.identity.resolveHandle(async ({ params, req }) => { let handle: string try { handle = ident.normalizeAndEnsureValidHandle(params.handle) @@ -34,7 +34,7 @@ export default function (server: Server, ctx: AppContext) { // this is not someone on our server, but we help with resolving anyway - if (!did && ctx.cfg.bskyAppViewEndpoint) { + if (!did && ctx.canProxyRead(req)) { did = await tryResolveFromAppview(ctx.appviewAgent, handle) } diff --git a/packages/pds/src/api/com/atproto/repo/getRecord.ts b/packages/pds/src/api/com/atproto/repo/getRecord.ts index 020935eaffa..bec4fff1e8c 100644 --- a/packages/pds/src/api/com/atproto/repo/getRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/getRecord.ts @@ -26,7 +26,7 @@ export default function (server: Server, ctx: AppContext) { } } - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.com.atproto.repo.getRecord(params) return { encoding: 'application/json', diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts b/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts index 850bd1015de..df37174f7e5 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts b/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts index bcc129b8de2..673868df384 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.actor.getProfiles( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts b/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts index 7b7f8d1289d..540f7795122 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts @@ -7,7 +7,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.actor.getSuggestions( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts b/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts index f3f95460686..380fea56ae4 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts @@ -16,7 +16,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.actor.searchActors( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts index f0868a402ec..2cf879af954 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts @@ -14,7 +14,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.actor.searchActorsTypeahead( params, diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts b/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts index fe6ac23416f..6cc8becbeac 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getActorFeeds( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index f30e5d2773a..6a349dd6ccf 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -10,7 +10,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts index e521f4fda34..ca5c09ce39e 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts @@ -25,7 +25,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const { data: feed } = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( { feed: params.feed }, diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts index ec874565ebe..7f4de6fa327 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts @@ -12,7 +12,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts index a7879dd0934..0e504abd39d 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerators( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts b/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts index a29f69a8be7..262dd678f39 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getLikes( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts index 90244bf2e3f..da8f72dfde2 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts @@ -27,7 +27,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts index cc6c39ec44b..2572702d78d 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getPosts( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts b/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts index 5d5948954b5..4632883d0ef 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getRepostedBy( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts b/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts index 1b3e6271b50..3587c3acc7d 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts @@ -16,7 +16,17 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`) } - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { + const res = await ctx.appviewAgent.api.app.bsky.feed.getTimeline( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + } + if (ctx.canProxyFeedConstruction(req)) { const res = await ctx.appviewAgent.api.app.bsky.unspecced.getTimelineSkeleton( { limit, cursor }, diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts b/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts index e14a12bd37a..e76eb2963a1 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.graph.getBlocks( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts index 3b3281edb41..93bf73075e0 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts @@ -9,7 +9,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.graph.getFollowers( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts b/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts index 4a8bf888fc6..75edf58c225 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts @@ -9,7 +9,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.graph.getFollows( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getList.ts b/packages/pds/src/app-view/api/app/bsky/graph/getList.ts index 77b16e9677c..c711ea93026 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getList.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getList.ts @@ -9,7 +9,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.graph.getList( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts b/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts index e3a486ccf0b..89501548f40 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.graph.getListMutes( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts b/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts index cb77e2bf2e8..7dc77e6c5ef 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.graph.getLists( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts b/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts index 98218309800..51950515cd4 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.graph.getMutes( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts b/packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts index ce3dfcf4125..ac22ebd2bba 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts @@ -23,7 +23,7 @@ export default function (server: Server, ctx: AppContext) { mutedByDid: requester, }) - if (ctx.cfg.bskyAppViewEndpoint) { + if (ctx.canProxyWrite()) { await ctx.appviewAgent.api.app.bsky.graph.muteActor(input.body, { ...(await ctx.serviceAuthHeaders(requester)), encoding: 'application/json', diff --git a/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts b/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts index 982a652f7b8..65c09125f45 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts @@ -22,7 +22,7 @@ export default function (server: Server, ctx: AppContext) { mutedByDid: requester, }) - if (ctx.cfg.bskyAppViewEndpoint) { + if (ctx.canProxyWrite()) { await ctx.appviewAgent.api.app.bsky.graph.muteActorList(input.body, { ...(await ctx.serviceAuthHeaders(requester)), encoding: 'application/json', diff --git a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts b/packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts index ae9e8dac742..1b911e71336 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts @@ -20,7 +20,7 @@ export default function (server: Server, ctx: AppContext) { mutedByDid: requester, }) - if (ctx.cfg.bskyAppViewEndpoint) { + if (ctx.canProxyWrite()) { await ctx.appviewAgent.api.app.bsky.graph.unmuteActor(input.body, { ...(await ctx.serviceAuthHeaders(requester)), encoding: 'application/json', diff --git a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts b/packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts index 69012072100..36e65e4178a 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts @@ -13,7 +13,7 @@ export default function (server: Server, ctx: AppContext) { mutedByDid: requester, }) - if (ctx.cfg.bskyAppViewEndpoint) { + if (ctx.canProxyWrite()) { await ctx.appviewAgent.api.app.bsky.graph.unmuteActorList(input.body, { ...(await ctx.serviceAuthHeaders(requester)), encoding: 'application/json', diff --git a/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts b/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts index e16e2be46ce..c1b7c276d9f 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.notification.getUnreadCount( params, diff --git a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts b/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts index c9ec4885e74..2967c069ad9 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts +++ b/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts @@ -11,7 +11,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.notification.listNotifications( params, diff --git a/packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts b/packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts index 717f358f77c..f5644abfe7f 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts +++ b/packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts @@ -27,7 +27,7 @@ export default function (server: Server, ctx: AppContext) { .where('did', '=', user.did) .executeTakeFirst() - if (ctx.cfg.bskyAppViewEndpoint) { + if (ctx.canProxyWrite()) { await ctx.appviewAgent.api.app.bsky.notification.updateSeen( input.body, { diff --git a/packages/pds/src/app-view/api/app/bsky/unspecced.ts b/packages/pds/src/app-view/api/app/bsky/unspecced.ts index 12b6db691eb..5688549fbe7 100644 --- a/packages/pds/src/app-view/api/app/bsky/unspecced.ts +++ b/packages/pds/src/app-view/api/app/bsky/unspecced.ts @@ -23,7 +23,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxy(req)) { + if (ctx.canProxyRead(req)) { const hotClassicUri = Object.keys(ctx.algos).find((uri) => uri.endsWith('/hot-classic'), ) diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index c884721b1e5..b860cb75908 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -60,6 +60,7 @@ export interface ServerConfigValues { bskyAppViewEndpoint?: string bskyAppViewDid?: string + bskyAppViewProxy: boolean crawlersToNotify?: string[] } @@ -181,6 +182,8 @@ export class ServerConfig { process.env.BSKY_APP_VIEW_ENDPOINT, ) const bskyAppViewDid = nonemptyString(process.env.BSKY_APP_VIEW_DID) + const bskyAppViewProxy = + process.env.BSKY_APP_VIEW_PROXY === 'true' ? true : false const crawlersEnv = process.env.CRAWLERS_TO_NOTIFY const crawlersToNotify = @@ -229,6 +232,7 @@ export class ServerConfig { dbTxLockNonce, bskyAppViewEndpoint, bskyAppViewDid, + bskyAppViewProxy, crawlersToNotify, ...overrides, }) @@ -432,6 +436,10 @@ export class ServerConfig { return this.cfg.bskyAppViewDid } + get bskyAppViewProxy() { + return this.cfg.bskyAppViewProxy + } + get crawlersToNotify() { return this.cfg.crawlersToNotify } diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 75128a0d0ae..1e3ce8167d4 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -174,12 +174,26 @@ export class AppContext { return this._appviewAgent } - canProxy(req: express.Request): boolean { + canProxyRead(req: express.Request): boolean { return ( + this.cfg.bskyAppViewProxy && this.cfg.bskyAppViewEndpoint !== undefined && req.get('x-appview-proxy') !== undefined ) } + + canProxyFeedConstruction(req: express.Request): boolean { + return ( + this.cfg.bskyAppViewEndpoint !== undefined && + req.get('x-appview-proxy') !== undefined + ) + } + + canProxyWrite(): boolean { + return ( + this.cfg.bskyAppViewProxy && this.cfg.bskyAppViewEndpoint !== undefined + ) + } } export default AppContext diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index b12c3cffad3..225b3a85a1d 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -106,6 +106,7 @@ export const runTestServer = async ( repoBackfillLimitMs: HOUR, sequencerLeaderLockId: uniqueLockId(), dbTxLockNonce: await randomStr(32, 'base32'), + bskyAppViewProxy: false, ...params, }) diff --git a/packages/pds/tests/proxied/timeline-skeleton.test.ts b/packages/pds/tests/proxied/timeline-skeleton.test.ts index 680dd3423d4..2131150e3a3 100644 --- a/packages/pds/tests/proxied/timeline-skeleton.test.ts +++ b/packages/pds/tests/proxied/timeline-skeleton.test.ts @@ -15,7 +15,8 @@ describe('proxies timeline skeleton', () => { network = await TestNetwork.create({ dbPostgresSchema: 'proxy_timeline_skeleton', pds: { - enableAppView: true, + enableInProcessAppView: true, + bskyAppViewProxy: false, }, }) agent = network.pds.getClient() From 03200c1d8b09d248fa7a9d287cb99db045285dba Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Mon, 3 Jul 2023 21:21:16 +0200 Subject: [PATCH 015/237] :bug: Only ignore reports for specific at-uri when ignoreSubject contains at-uri (#1251) --- .../bsky/src/services/moderation/index.ts | 27 ++++++++++++++--- packages/pds/src/services/moderation/index.ts | 26 ++++++++++++++--- .../admin/get-moderation-reports.test.ts | 29 +++++++++++++++++-- 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/packages/bsky/src/services/moderation/index.ts b/packages/bsky/src/services/moderation/index.ts index 4d86b4fa9b3..99715866d2a 100644 --- a/packages/bsky/src/services/moderation/index.ts +++ b/packages/bsky/src/services/moderation/index.ts @@ -104,12 +104,31 @@ export class ModerationService { .orWhere('subjectUri', '=', subject) }) } + if (ignoreSubjects?.length) { - builder = builder.where((qb) => { - return qb - .where('subjectDid', 'not in', ignoreSubjects) - .where('subjectUri', 'not in', ignoreSubjects) + const ignoreUris: string[] = [] + const ignoreDids: string[] = [] + + ignoreSubjects.forEach((subject) => { + if (subject.startsWith('at://')) { + ignoreUris.push(subject) + } else if (subject.startsWith('did:')) { + ignoreDids.push(subject) + } }) + + if (ignoreDids.length) { + builder = builder.where('subjectDid', 'not in', ignoreDids) + } + if (ignoreUris.length) { + builder = builder.where((qb) => { + // Without the null condition, postgres will ignore all reports where `subjectUri` is null + // which will make all the account reports be ignored as well + return qb + .where('subjectUri', 'not in', ignoreUris) + .orWhere('subjectUri', 'is', null) + }) + } } if (reporters?.length) { diff --git a/packages/pds/src/services/moderation/index.ts b/packages/pds/src/services/moderation/index.ts index 4a7b3c9eefc..18e3ceb5608 100644 --- a/packages/pds/src/services/moderation/index.ts +++ b/packages/pds/src/services/moderation/index.ts @@ -124,11 +124,29 @@ export class ModerationService { } if (ignoreSubjects?.length) { - builder = builder.where((qb) => { - return qb - .where('subjectDid', 'not in', ignoreSubjects) - .where('subjectUri', 'not in', ignoreSubjects) + const ignoreUris: string[] = [] + const ignoreDids: string[] = [] + + ignoreSubjects.forEach((subject) => { + if (subject.startsWith('at://')) { + ignoreUris.push(subject) + } else if (subject.startsWith('did:')) { + ignoreDids.push(subject) + } }) + + if (ignoreDids.length) { + builder = builder.where('subjectDid', 'not in', ignoreDids) + } + if (ignoreUris.length) { + builder = builder.where((qb) => { + // Without the null condition, postgres will ignore all reports where `subjectUri` is null + // which will make all the account reports be ignored as well + return qb + .where('subjectUri', 'not in', ignoreUris) + .orWhere('subjectUri', 'is', null) + }) + } } if (reporters?.length) { diff --git a/packages/pds/tests/views/admin/get-moderation-reports.test.ts b/packages/pds/tests/views/admin/get-moderation-reports.test.ts index 144a75a9f83..0ef0f92685c 100644 --- a/packages/pds/tests/views/admin/get-moderation-reports.test.ts +++ b/packages/pds/tests/views/admin/get-moderation-reports.test.ts @@ -136,15 +136,40 @@ describe('pds admin get moderation reports view', () => { const ignoreSubjects = getDids(allReports).slice(0, 2) - const filteredReports = + const filteredReportsByDid = await agent.api.com.atproto.admin.getModerationReports( { ignoreSubjects }, { headers: { authorization: adminAuth() } }, ) - getDids(filteredReports).forEach((resultDid) => + // Validate that when ignored by DID, all reports for that DID is ignored + getDids(filteredReportsByDid).forEach((resultDid) => expect(ignoreSubjects).not.toContain(resultDid), ) + + const ignoredAtUriSubjects: string[] = [ + `${ + allReports.data.reports.find(({ subject }) => !!subject.uri)?.subject + ?.uri + }`, + ] + const filteredReportsByAtUri = + await agent.api.com.atproto.admin.getModerationReports( + { + ignoreSubjects: ignoredAtUriSubjects, + }, + { headers: { authorization: adminAuth() } }, + ) + + // Validate that when ignored by at uri, only the reports for that at uri is ignored + expect(filteredReportsByAtUri.data.reports.length).toEqual( + allReports.data.reports.length - 1, + ) + expect( + filteredReportsByAtUri.data.reports + .map(({ subject }) => subject.uri) + .filter(Boolean), + ).not.toContain(ignoredAtUriSubjects[0]) }) it('gets all moderation reports.', async () => { From 8815efd86eb298e5809d848d59735d2ec0947f2a Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 3 Jul 2023 15:03:04 -0500 Subject: [PATCH 016/237] Move timeline construction to appview (#1274) full switch timeline construction to appview --- packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts b/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts index 3587c3acc7d..dba1b58ece7 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts @@ -26,7 +26,8 @@ export default function (server: Server, ctx: AppContext) { body: res.data, } } - if (ctx.canProxyFeedConstruction(req)) { + + if (ctx.cfg.bskyAppViewEndpoint) { const res = await ctx.appviewAgent.api.app.bsky.unspecced.getTimelineSkeleton( { limit, cursor }, From 24be348dfbda473f40acff62d619bc5e1bb354a4 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 3 Jul 2023 20:07:20 -0500 Subject: [PATCH 017/237] Better propagate errors on repo streams (#1276) better propgate errors on repo streams --- packages/repo/src/util.ts | 25 ++++++- packages/repo/tests/util.test.ts | 21 ++++++ packages/xrpc-server/src/server.ts | 1 + packages/xrpc-server/tests/responses.test.ts | 77 ++++++++++++++++++++ 4 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 packages/repo/tests/util.test.ts create mode 100644 packages/xrpc-server/tests/responses.test.ts diff --git a/packages/repo/src/util.ts b/packages/repo/src/util.ts index 8ec5239fa4f..42a59f6ea82 100644 --- a/packages/repo/src/util.ts +++ b/packages/repo/src/util.ts @@ -10,6 +10,7 @@ import { check, schema, cidForCbor, + byteIterableToStream, } from '@atproto/common' import { ipldToLex, lexToIpld, LexValue, RepoRecord } from '@atproto/lexicon' @@ -33,6 +34,7 @@ import BlockMap from './block-map' import { MissingBlocksError } from './error' import * as parse from './parse' import { Keypair } from '@atproto/crypto' +import { Readable } from 'stream' export async function* verifyIncomingCarBlocks( car: AsyncIterable, @@ -43,16 +45,31 @@ export async function* verifyIncomingCarBlocks( } } -export const writeCar = ( +// we have to turn the car writer output into a stream in order to properly handle errors +export function writeCarStream( root: CID | null, fn: (car: BlockWriter) => Promise, -): AsyncIterable => { +): Readable { const { writer, out } = root !== null ? CarWriter.create(root) : CarWriter.create() - fn(writer).finally(() => writer.close()) + const stream = byteIterableToStream(out) + fn(writer) + .catch((err) => { + stream.destroy(err) + }) + .finally(() => writer.close()) + return stream +} - return out +export async function* writeCar( + root: CID | null, + fn: (car: BlockWriter) => Promise, +): AsyncIterable { + const stream = writeCarStream(root, fn) + for await (const chunk of stream) { + yield chunk + } } export const blocksToCarStream = ( diff --git a/packages/repo/tests/util.test.ts b/packages/repo/tests/util.test.ts new file mode 100644 index 00000000000..f341cadfea9 --- /dev/null +++ b/packages/repo/tests/util.test.ts @@ -0,0 +1,21 @@ +import { dataToCborBlock, wait } from '@atproto/common' +import { writeCar } from '../src' + +describe('Utils', () => { + describe('writeCar()', () => { + it('propagates errors', async () => { + const iterate = async () => { + const iter = writeCar(null, async (car) => { + await wait(1) + const block = await dataToCborBlock({ test: 1 }) + await car.put(block) + throw new Error('Oops!') + }) + for await (const bytes of iter) { + // no-op + } + } + await expect(iterate).rejects.toThrow('Oops!') + }) + }) +}) diff --git a/packages/xrpc-server/src/server.ts b/packages/xrpc-server/src/server.ts index 2c30ae8a440..f8dc5c82f8c 100644 --- a/packages/xrpc-server/src/server.ts +++ b/packages/xrpc-server/src/server.ts @@ -235,6 +235,7 @@ export class Server { } else if (output?.body instanceof Readable) { res.header('Content-Type', output.encoding) res.status(200) + res.once('error', (err) => res.destroy(err)) forwardStreamErrors(output.body, res) output.body.pipe(res) } else if (output) { diff --git a/packages/xrpc-server/tests/responses.test.ts b/packages/xrpc-server/tests/responses.test.ts new file mode 100644 index 00000000000..0eaccba0633 --- /dev/null +++ b/packages/xrpc-server/tests/responses.test.ts @@ -0,0 +1,77 @@ +import * as http from 'http' +import getPort from 'get-port' +import xrpc, { ServiceClient } from '@atproto/xrpc' +import { byteIterableToStream } from '@atproto/common' +import { createServer, closeServer } from './_util' +import * as xrpcServer from '../src' + +const LEXICONS = [ + { + lexicon: 1, + id: 'io.example.readableStream', + defs: { + main: { + type: 'query', + parameters: { + type: 'params', + properties: { + shouldErr: { type: 'boolean' }, + }, + }, + output: { + encoding: 'application/vnd.ipld.car', + }, + }, + }, + }, +] + +describe('Responses', () => { + let s: http.Server + const server = xrpcServer.createServer(LEXICONS) + server.method( + 'io.example.readableStream', + async (ctx: { params: xrpcServer.Params }) => { + async function* iter(): AsyncIterable { + for (let i = 0; i < 5; i++) { + yield new Uint8Array([i]) + } + if (ctx.params.shouldErr) { + throw new Error('error') + } + } + return { + encoding: 'application/vnd.ipld.car', + body: byteIterableToStream(iter()), + } + }, + ) + xrpc.addLexicons(LEXICONS) + + let client: ServiceClient + let url: string + beforeAll(async () => { + const port = await getPort() + s = await createServer(port, server) + url = `http://localhost:${port}` + client = xrpc.service(url) + }) + afterAll(async () => { + await closeServer(s) + }) + + it('returns readable streams of bytes', async () => { + const res = await client.call('io.example.readableStream', { + shouldErr: false, + }) + const expected = new Uint8Array([0, 1, 2, 3, 4]) + expect(res.data).toEqual(expected) + }) + + it('handles errs on readable streams of bytes', async () => { + const attempt = client.call('io.example.readableStream', { + shouldErr: true, + }) + await expect(attempt).rejects.toThrow() + }) +}) From dd4d91ce6b31031d393fd1334640bc987ae513b8 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Tue, 4 Jul 2023 00:15:13 -0400 Subject: [PATCH 018/237] Log pds sequencer leader stats (#1277) log pds sequencer leader stats --- packages/pds/src/index.ts | 12 +++++++++++- packages/pds/src/sequencer/sequencer-leader.ts | 10 +++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 3f61323da8f..a56fa893930 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -21,7 +21,7 @@ import Database from './db' import { ServerAuth } from './auth' import * as error from './error' import compression from './util/compression' -import { dbLogger, loggerMiddleware } from './logger' +import { dbLogger, loggerMiddleware, seqLogger } from './logger' import { ServerConfig } from './config' import { ServerMailer } from './mailer' import { createServer } from './lexicon' @@ -56,6 +56,7 @@ export class PDS { public server?: http.Server private terminator?: HttpTerminator private dbStatsInterval?: NodeJS.Timer + private sequencerStatsInterval?: NodeJS.Timer constructor(opts: { ctx: AppContext @@ -249,6 +250,14 @@ export class PDS { ) }, 10000) } + this.sequencerStatsInterval = setInterval(() => { + if (this.ctx.sequencerLeader.isLeader) { + seqLogger.info( + { seq: this.ctx.sequencerLeader.peekSeqVal() }, + 'sequencer leader stats', + ) + } + }, 500) appviewConsumers.listen(this.ctx) this.ctx.sequencerLeader.run() await this.ctx.sequencer.start() @@ -267,6 +276,7 @@ export class PDS { await this.ctx.backgroundQueue.destroy() await this.ctx.db.close() clearInterval(this.dbStatsInterval) + clearInterval(this.sequencerStatsInterval) } } diff --git a/packages/pds/src/sequencer/sequencer-leader.ts b/packages/pds/src/sequencer/sequencer-leader.ts index 58eb9aaee5f..c951fda0d25 100644 --- a/packages/pds/src/sequencer/sequencer-leader.ts +++ b/packages/pds/src/sequencer/sequencer-leader.ts @@ -12,7 +12,7 @@ export class SequencerLeader { destroyed = false polling = false queued = false - lastSeq: number + private lastSeq: number constructor(public db: Database, lockId = SEQUENCER_LEADER_ID) { this.leader = new Leader(lockId, this.db) @@ -23,6 +23,14 @@ export class SequencerLeader { return this.lastSeq } + peekSeqVal(): number | undefined { + return this.lastSeq + } + + get isLeader() { + return !!this.leader.session + } + async run() { if (this.db.dialect === 'sqlite') return From 0ceed96b11067244d73ea944bfd2a857dc2f069f Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 4 Jul 2023 18:40:43 -0500 Subject: [PATCH 019/237] Explicit dns servers (#1281) * add ability to setup explicit dns servers * cleanup * fix * reorder * pr feedback --- packages/identity/src/handle/index.ts | 56 +++++++++++++++++++++++---- packages/identity/src/types.ts | 2 + packages/pds/src/config.ts | 10 +++++ packages/pds/src/index.ts | 6 ++- 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/packages/identity/src/handle/index.ts b/packages/identity/src/handle/index.ts index fa9c016cd99..8780437efa7 100644 --- a/packages/identity/src/handle/index.ts +++ b/packages/identity/src/handle/index.ts @@ -6,9 +6,12 @@ const PREFIX = 'did=' export class HandleResolver { public timeout: number + private backupNameservers: string[] | undefined + private backupNameserverIps: string[] | undefined constructor(opts: HandleResolverOpts = {}) { this.timeout = opts.timeout ?? 3000 + this.backupNameservers = opts.backupNameservers } async resolve(handle: string): Promise { @@ -23,7 +26,11 @@ export class HandleResolver { httpAbort.abort() return dnsRes } - return httpPromise + const res = await httpPromise + if (res) { + return res + } + return this.resolveDnsBackup(handle) } async resolveDns(handle: string): Promise { @@ -33,12 +40,7 @@ export class HandleResolver { } catch (err) { return undefined } - const results = chunkedResults.map((chunks) => chunks.join('')) - const found = results.filter((i) => i.startsWith(PREFIX)) - if (found.length !== 1) { - return undefined - } - return found[0].slice(PREFIX.length) + return this.parseDnsResult(chunkedResults) } async resolveHttp( @@ -57,4 +59,44 @@ export class HandleResolver { return undefined } } + + async resolveDnsBackup(handle: string): Promise { + let chunkedResults: string[][] + try { + const backupIps = await this.getBackupNameserverIps() + if (!backupIps || backupIps.length < 1) return undefined + const resolver = new dns.Resolver() + resolver.setServers(backupIps) + chunkedResults = await resolver.resolveTxt(`${SUBDOMAIN}.${handle}`) + } catch (err) { + return undefined + } + return this.parseDnsResult(chunkedResults) + } + + parseDnsResult(chunkedResults: string[][]): string | undefined { + const results = chunkedResults.map((chunks) => chunks.join('')) + const found = results.filter((i) => i.startsWith(PREFIX)) + if (found.length !== 1) { + return undefined + } + return found[0].slice(PREFIX.length) + } + + private async getBackupNameserverIps(): Promise { + if (!this.backupNameservers) { + return undefined + } else if (!this.backupNameserverIps) { + const responses = await Promise.allSettled( + this.backupNameservers.map((h) => dns.lookup(h)), + ) + for (const res of responses) { + if (res.status === 'fulfilled') { + this.backupNameserverIps ??= [] + this.backupNameserverIps.push(res.value.address) + } + } + } + return this.backupNameserverIps + } } diff --git a/packages/identity/src/types.ts b/packages/identity/src/types.ts index fdb1857f3b5..f1d983e6742 100644 --- a/packages/identity/src/types.ts +++ b/packages/identity/src/types.ts @@ -4,10 +4,12 @@ export type IdentityResolverOpts = { timeout?: number plcUrl?: string didCache?: DidCache + backupNameservers?: string[] } export type HandleResolverOpts = { timeout?: number + backupNameservers?: string[] } export type DidResolverOpts = { diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index b860cb75908..282f7929a10 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -35,6 +35,7 @@ export interface ServerConfigValues { databaseLocation?: string availableUserDomains: string[] + handleResolveNameservers?: string[] imgUriSalt: string imgUriKey: string @@ -136,6 +137,10 @@ export class ServerConfig { ? process.env.AVAILABLE_USER_DOMAINS.split(',') : [] + const handleResolveNameservers = process.env.HANDLE_RESOLVE_NAMESERVERS + ? process.env.HANDLE_RESOLVE_NAMESERVERS.split(',') + : [] + const imgUriSalt = process.env.IMG_URI_SALT || '9dd04221f5755bce5f55f47464c27e1e' const imgUriKey = @@ -215,6 +220,7 @@ export class ServerConfig { termsOfServiceUrl, databaseLocation, availableUserDomains, + handleResolveNameservers, imgUriSalt, imgUriKey, imgUriEndpoint, @@ -368,6 +374,10 @@ export class ServerConfig { return this.cfg.availableUserDomains } + get handleResolveNameservers() { + return this.cfg.handleResolveNameservers + } + get imgUriSalt() { return this.cfg.imgUriSalt } diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index a56fa893930..f77b654a069 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -96,7 +96,11 @@ export class PDS { config.didCacheStaleTTL, config.didCacheMaxTTL, ) - const idResolver = new IdResolver({ plcUrl: config.didPlcUrl, didCache }) + const idResolver = new IdResolver({ + plcUrl: config.didPlcUrl, + didCache, + backupNameservers: config.handleResolveNameservers, + }) const messageDispatcher = new MessageDispatcher() const sequencer = new Sequencer(db) From 4f7fd8b1180e07f4d8531ad0aab1e185392b2dc4 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 4 Jul 2023 20:58:06 -0500 Subject: [PATCH 020/237] Thread through id-resolver cfg (#1282) thread through id-resolver-cfg --- packages/identity/src/id-resolver.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/identity/src/id-resolver.ts b/packages/identity/src/id-resolver.ts index e400e201832..ccf42ca9574 100644 --- a/packages/identity/src/id-resolver.ts +++ b/packages/identity/src/id-resolver.ts @@ -8,7 +8,10 @@ export class IdResolver { constructor(opts: IdentityResolverOpts = {}) { const { timeout = 3000, plcUrl, didCache } = opts - this.handle = new HandleResolver({ timeout }) + this.handle = new HandleResolver({ + timeout, + backupNameservers: opts.backupNameservers, + }) this.did = new DidResolver({ timeout, plcUrl, didCache }) } } From bb2848e7f5f588c457a8ed117ed1fa7f72af51bf Mon Sep 17 00:00:00 2001 From: devin ivy Date: Wed, 5 Jul 2023 15:01:13 -0400 Subject: [PATCH 021/237] Bsky log commit error (#1275) * don't bail on bad record index * add build * temporarily disable check, full reindex on rabase * don't bail on bad record index during rebase, track last commit on rebase * log bsky repo subscription stats * add running and waiting count to repo sub stats * re-enable fast path for happy rebases * only hold onto seq in cursor consecutivelist, don't hold onto whole completed messages --- .../workflows/build-and-push-bsky-aws.yaml | 1 + packages/bsky/src/index.ts | 17 +++++++- packages/bsky/src/services/indexing/index.ts | 5 ++- packages/bsky/src/subscription/repo.ts | 42 ++++++++++++++----- 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-and-push-bsky-aws.yaml b/.github/workflows/build-and-push-bsky-aws.yaml index b656dec89c9..9e5c2e9b870 100644 --- a/.github/workflows/build-and-push-bsky-aws.yaml +++ b/.github/workflows/build-and-push-bsky-aws.yaml @@ -3,6 +3,7 @@ on: push: branches: - main + - bsky-log-commit-error env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 771b0ef27c3..8af95aba95c 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -9,7 +9,7 @@ import { IdResolver } from '@atproto/identity' import API, { health, blobResolver } from './api' import Database from './db' import * as error from './error' -import { dbLogger, loggerMiddleware } from './logger' +import { dbLogger, loggerMiddleware, subLogger } from './logger' import { ServerConfig } from './config' import { createServer } from './lexicon' import { ImageUriBuilder } from './image/uri' @@ -41,6 +41,7 @@ export class BskyAppView { public server?: http.Server private terminator?: HttpTerminator private dbStatsInterval: NodeJS.Timer + private subStatsInterval: NodeJS.Timer constructor(opts: { ctx: AppContext @@ -188,6 +189,19 @@ export class BskyAppView { 'background queue stats', ) }, 10000) + if (this.sub) { + this.subStatsInterval = setInterval(() => { + subLogger.info( + { + seq: this.sub?.lastSeq, + cursor: this.sub?.lastCursor, + runningCount: this.sub?.repoQueue.main.pending, + waitingCount: this.sub?.repoQueue.main.size, + }, + 'repo subscription stats', + ) + }, 500) + } const server = this.app.listen(this.ctx.cfg.port) this.server = server this.terminator = createHttpTerminator({ server }) @@ -201,6 +215,7 @@ export class BskyAppView { async destroy(): Promise { await this.ctx.didCache.destroy() await this.sub?.destroy() + clearInterval(this.subStatsInterval) await this.terminator?.terminate() await this.ctx.backgroundQueue.destroy() await this.ctx.db.close() diff --git a/packages/bsky/src/services/indexing/index.ts b/packages/bsky/src/services/indexing/index.ts index da79cbb6345..25ecb8ccebe 100644 --- a/packages/bsky/src/services/indexing/index.ts +++ b/packages/bsky/src/services/indexing/index.ts @@ -203,7 +203,10 @@ export class IndexingService { 'skipping indexing of invalid record', ) } else { - throw err + subLogger.error( + { err, did, commit, uri: uri.toString(), cid: cid.toString() }, + 'skipping indexing due to error processing record', + ) } } }) diff --git a/packages/bsky/src/subscription/repo.ts b/packages/bsky/src/subscription/repo.ts index 2f4047805d1..db0f84dca8f 100644 --- a/packages/bsky/src/subscription/repo.ts +++ b/packages/bsky/src/subscription/repo.ts @@ -27,8 +27,10 @@ export class RepoSubscription { leader = new Leader(this.subLockId, this.ctx.db) repoQueue: PartitionedQueue cursorQueue = new LatestQueue() - consecutive = new ConsecutiveList() + consecutive = new ConsecutiveList() destroyed = false + lastSeq: number | undefined + lastCursor: number | undefined constructor( public ctx: AppContext, @@ -56,7 +58,8 @@ export class RepoSubscription { ) continue } - const item = this.consecutive.push(details.message) + this.lastSeq = details.seq + const item = this.consecutive.push(details.seq) this.repoQueue .add(details.repo, () => this.handleMessage(details.message)) .catch((err) => { @@ -113,7 +116,7 @@ export class RepoSubscription { this.destroyed = false this.repoQueue = new PartitionedQueue({ concurrency: this.concurrency }) this.cursorQueue = new LatestQueue() - this.consecutive = new ConsecutiveList() + this.consecutive = new ConsecutiveList() await this.run() } @@ -138,14 +141,19 @@ export class RepoSubscription { const indexRecords = async () => { const { root, rootCid, ops } = await getOps(msg) if (msg.tooBig) { - return await indexingService.indexRepo(msg.repo, rootCid.toString()) + await indexingService.indexRepo(msg.repo, rootCid.toString()) + await indexingService.setCommitLastSeen(root, msg) + return } if (msg.rebase) { const needsReindex = await indexingService.checkCommitNeedsIndexing( root, ) - if (!needsReindex) return - return await indexingService.indexRepo(msg.repo, rootCid.toString()) + if (needsReindex) { + await indexingService.indexRepo(msg.repo, rootCid.toString()) + } + await indexingService.setCommitLastSeen(root, msg) + return } for (const op of ops) { if (op.action === WriteOpAction.Delete) { @@ -171,7 +179,16 @@ export class RepoSubscription { 'skipping indexing of invalid record', ) } else { - throw err + subLogger.error( + { + err, + did: msg.repo, + commit: msg.commit.toString(), + uri: op.uri.toString(), + cid: op.cid.toString(), + }, + 'skipping indexing due to error processing record', + ) } } } @@ -195,10 +212,10 @@ export class RepoSubscription { await services.indexing(db).tombstoneActor(msg.did) } - private async handleCursor(msg: ProcessableMessage) { + private async handleCursor(seq: number) { const { db } = this.ctx await db.transaction(async (tx) => { - await this.setState(tx, { cursor: msg.seq }) + await this.setState(tx, { cursor: seq }) }) } @@ -209,7 +226,9 @@ export class RepoSubscription { .where('service', '=', this.service) .where('method', '=', METHOD) .executeTakeFirst() - return sub ? (JSON.parse(sub.state) as State) : { cursor: 0 } + const state = sub ? (JSON.parse(sub.state) as State) : { cursor: 0 } + this.lastCursor = state.cursor + return state } async resetState(): Promise { @@ -222,6 +241,9 @@ export class RepoSubscription { private async setState(tx: Database, state: State): Promise { tx.assertTransaction() + tx.onCommit(() => { + this.lastCursor = state.cursor + }) const res = await tx.db .updateTable('subscription') .where('service', '=', this.service) From 4d1f8d32899b765e17af219329e2e3166b43e54d Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 5 Jul 2023 16:41:08 -0500 Subject: [PATCH 022/237] Misc scaling (#1284) * limit backsearch to 1 day instead of 3 * lower like count threshold * bump to 6 * disable like count check * disable with friends * preemptively cache last commit * inline list mutes * actor service * label cache * placehodler on popular with friends * bulk sequence * no limit but chunk * bump chunk to 5k * try 10k * fix notify * tweaking * syntax * one more fix * increase backfill allowance * full refresh label cache * limit 1 on mute list * reserve aclu handle * clean up testing with label cache * note on with-friends * rm defer from label cache * label cache error handling * rm branch build --- packages/dev-env/src/bsky.ts | 4 + packages/dev-env/src/network-no-appview.ts | 4 + packages/dev-env/src/network.ts | 6 +- packages/dev-env/src/pds.ts | 8 ++ packages/identifier/src/reserved.ts | 1 + .../src/app-view/api/app/bsky/util/feed.ts | 2 +- .../pds/src/app-view/services/actor/index.ts | 13 ++- .../pds/src/app-view/services/actor/views.ts | 103 ++++++++++-------- .../pds/src/app-view/services/feed/index.ts | 43 ++++++-- .../pds/src/app-view/services/graph/index.ts | 16 +++ .../pds/src/app-view/services/label/index.ts | 37 +++++-- packages/pds/src/context.ts | 6 + packages/pds/src/feed-gen/with-friends.ts | 73 ++++++++----- packages/pds/src/index.ts | 7 ++ packages/pds/src/label-cache.ts | 90 +++++++++++++++ packages/pds/src/sequencer/outbox.ts | 4 +- .../pds/src/sequencer/sequencer-leader.ts | 22 ++-- packages/pds/src/sequencer/sequencer.ts | 2 +- packages/pds/src/services/index.ts | 9 +- packages/pds/src/services/repo/index.ts | 8 +- packages/pds/src/sql-repo-storage.ts | 18 +++ packages/pds/tests/_util.ts | 8 ++ packages/pds/tests/account-deletion.test.ts | 2 +- packages/pds/tests/algos/hot-classic.test.ts | 4 +- packages/pds/tests/algos/whats-hot.test.ts | 4 +- packages/pds/tests/algos/with-friends.test.ts | 2 +- packages/pds/tests/blob-deletes.test.ts | 8 +- packages/pds/tests/event-stream/sync.test.ts | 1 + packages/pds/tests/feed-generation.test.ts | 2 +- packages/pds/tests/indexing.test.ts | 12 +- packages/pds/tests/labeler/labeler.test.ts | 8 +- .../pds/tests/migrations/blob-creator.test.ts | 2 +- .../migrations/indexed-at-on-record.test.ts | 2 +- .../tests/migrations/post-hierarchy.test.ts | 2 +- .../migrations/repo-sync-data-pt2.test.ts | 2 +- .../tests/migrations/repo-sync-data.test.ts | 2 +- .../migrations/user-partitioned-cids.test.ts | 2 +- .../migrations/user-table-did-pkey.test.ts | 2 +- packages/pds/tests/moderation.test.ts | 2 +- packages/pds/tests/proxied/feedgen.test.ts | 1 - packages/pds/tests/views/actor-search.test.ts | 2 +- packages/pds/tests/views/author-feed.test.ts | 2 +- packages/pds/tests/views/blocks.test.ts | 2 +- packages/pds/tests/views/follows.test.ts | 2 +- packages/pds/tests/views/likes.test.ts | 2 +- packages/pds/tests/views/mute-lists.test.ts | 2 +- packages/pds/tests/views/mutes.test.ts | 2 +- .../pds/tests/views/notifications.test.ts | 4 +- packages/pds/tests/views/popular.test.ts | 4 +- packages/pds/tests/views/posts.test.ts | 2 +- packages/pds/tests/views/profile.test.ts | 2 +- packages/pds/tests/views/reposts.test.ts | 2 +- packages/pds/tests/views/suggestions.test.ts | 2 +- packages/pds/tests/views/thread.test.ts | 7 +- packages/pds/tests/views/timeline.test.ts | 2 +- 55 files changed, 414 insertions(+), 169 deletions(-) create mode 100644 packages/pds/src/label-cache.ts diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index 75206fe768c..da3010adf22 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -86,6 +86,10 @@ export class TestBsky { return new AtpAgent({ service: this.url }) } + async processAll() { + await this.ctx.backgroundQueue.processAll() + } + async close() { await this.server.destroy() } diff --git a/packages/dev-env/src/network-no-appview.ts b/packages/dev-env/src/network-no-appview.ts index f5244eff2db..24a0b72fb59 100644 --- a/packages/dev-env/src/network-no-appview.ts +++ b/packages/dev-env/src/network-no-appview.ts @@ -37,6 +37,10 @@ export class TestNetworkNoAppView { return fg } + async processAll() { + await this.pds.processAll() + } + async close() { await Promise.all(this.feedGens.map((fg) => fg.close())) await this.pds.close() diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index 5d589335346..1cce66cfde9 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -54,7 +54,6 @@ export class TestNetwork extends TestNetworkNoAppView { } async processFullSubscription(timeout = 5000) { - if (!this.bsky) return const sub = this.bsky.sub if (!sub) return const { db } = this.pds.ctx.db @@ -76,10 +75,9 @@ export class TestNetwork extends TestNetworkNoAppView { } async processAll(timeout?: number) { - await this.pds.ctx.backgroundQueue.processAll() - if (!this.bsky) return + await this.pds.processAll() await this.processFullSubscription(timeout) - await this.bsky.ctx.backgroundQueue.processAll() + await this.bsky.processAll() } async serviceHeaders(did: string, aud?: string) { diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index ee0c307f141..28b6727c4d8 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -96,6 +96,9 @@ export class TestPds { }) await server.start() + + // we refresh label cache by hand in `processAll` instead of on a timer + server.ctx.labelCache.stop() return new TestPds(url, port, server) } @@ -123,6 +126,11 @@ export class TestPds { } } + async processAll() { + await this.ctx.backgroundQueue.processAll() + await this.ctx.labelCache.fullRefresh() + } + async close() { await this.server.destroy() } diff --git a/packages/identifier/src/reserved.ts b/packages/identifier/src/reserved.ts index 83180160e61..c49c85f5378 100644 --- a/packages/identifier/src/reserved.ts +++ b/packages/identifier/src/reserved.ts @@ -864,6 +864,7 @@ const famousAccounts = [ // reserving some large twitter accounts (top 100 by followers according to wikidata dump) '10ronaldinho', '3gerardpique', + 'aclu', 'adele', 'akshaykumar', 'aliaa08', diff --git a/packages/pds/src/app-view/api/app/bsky/util/feed.ts b/packages/pds/src/app-view/api/app/bsky/util/feed.ts index 606580b3046..9d9d0323b95 100644 --- a/packages/pds/src/app-view/api/app/bsky/util/feed.ts +++ b/packages/pds/src/app-view/api/app/bsky/util/feed.ts @@ -12,7 +12,7 @@ export class FeedKeyset extends TimeCidKeyset { } // For users with sparse feeds, avoid scanning more than one week for a single page -export const getFeedDateThreshold = (from: string | undefined, days = 3) => { +export const getFeedDateThreshold = (from: string | undefined, days = 1) => { const timelineDateThreshold = from ? new Date(from) : new Date() timelineDateThreshold.setDate(timelineDateThreshold.getDate() - days) return timelineDateThreshold.toISOString() diff --git a/packages/pds/src/app-view/services/actor/index.ts b/packages/pds/src/app-view/services/actor/index.ts index 8ed28e4691b..e6c6fd6e756 100644 --- a/packages/pds/src/app-view/services/actor/index.ts +++ b/packages/pds/src/app-view/services/actor/index.ts @@ -3,15 +3,20 @@ import { DidHandle } from '../../../db/tables/did-handle' import { notSoftDeletedClause } from '../../../db/util' import { ActorViews } from './views' import { ImageUriBuilder } from '../../../image/uri' +import { LabelCache } from '../../../label-cache' export class ActorService { - constructor(public db: Database, public imgUriBuilder: ImageUriBuilder) {} + constructor( + public db: Database, + public imgUriBuilder: ImageUriBuilder, + public labelCache: LabelCache, + ) {} - static creator(imgUriBuilder: ImageUriBuilder) { - return (db: Database) => new ActorService(db, imgUriBuilder) + static creator(imgUriBuilder: ImageUriBuilder, labelCache: LabelCache) { + return (db: Database) => new ActorService(db, imgUriBuilder, labelCache) } - views = new ActorViews(this.db, this.imgUriBuilder) + views = new ActorViews(this.db, this.imgUriBuilder, this.labelCache) async getActor( handleOrDid: string, diff --git a/packages/pds/src/app-view/services/actor/views.ts b/packages/pds/src/app-view/services/actor/views.ts index 50fd021a78c..b0f652ef901 100644 --- a/packages/pds/src/app-view/services/actor/views.ts +++ b/packages/pds/src/app-view/services/actor/views.ts @@ -8,13 +8,19 @@ import { DidHandle } from '../../../db/tables/did-handle' import Database from '../../../db' import { ImageUriBuilder } from '../../../image/uri' import { LabelService } from '../label' -import { ListViewBasic } from '../../../lexicon/types/app/bsky/graph/defs' +import { GraphService } from '../graph' +import { LabelCache } from '../../../label-cache' export class ActorViews { - constructor(private db: Database, private imgUriBuilder: ImageUriBuilder) {} + constructor( + private db: Database, + private imgUriBuilder: ImageUriBuilder, + private labelCache: LabelCache, + ) {} services = { - label: LabelService.creator(), + label: LabelService.creator(this.labelCache)(this.db), + graph: GraphService.creator(this.imgUriBuilder)(this.db), } profileDetailed( @@ -82,18 +88,30 @@ export class ActorViews { .where('mutedByDid', '=', viewer) .select('did') .as('requesterMuted'), + this.db.db + .selectFrom('list_item') + .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') + .where('list_mute.mutedByDid', '=', viewer) + .whereRef('list_item.subjectDid', '=', ref('did_handle.did')) + .select('list_item.listUri') + .limit(1) + .as('requesterMutedByList'), ]) - const [profileInfos, labels, listMutes] = await Promise.all([ + const [profileInfos, labels] = await Promise.all([ profileInfosQb.execute(), - this.services.label(this.db).getLabelsForSubjects(dids), - this.getListMutes(dids, viewer), + this.services.label.getLabelsForSubjects(dids), ]) const profileInfoByDid = profileInfos.reduce((acc, info) => { return Object.assign(acc, { [info.did]: info }) }, {} as Record>) + const listUris: string[] = profileInfos + .map((a) => a.requesterMutedByList) + .filter((list) => !!list) + const listViews = await this.services.graph.getListViews(listUris, viewer) + const views = results.map((result) => { const profileInfo = profileInfoByDid[result.did] const avatar = profileInfo?.avatarCid @@ -114,8 +132,14 @@ export class ActorViews { postsCount: profileInfo?.postsCount || 0, indexedAt: profileInfo?.indexedAt || undefined, viewer: { - muted: !!profileInfo?.requesterMuted || !!listMutes[result.did], - mutedByList: listMutes[result.did], + muted: + !!profileInfo?.requesterMuted || + !!profileInfo?.requesterMutedByList, + mutedByList: profileInfo.requesterMutedByList + ? this.services.graph.formatListViewBasic( + listViews[profileInfo.requesterMutedByList], + ) + : undefined, blockedBy: !!profileInfo.requesterBlockedBy, blocking: profileInfo.requesterBlocking || undefined, following: profileInfo?.requesterFollowing || undefined, @@ -181,18 +205,30 @@ export class ActorViews { .where('mutedByDid', '=', viewer) .select('did') .as('requesterMuted'), + this.db.db + .selectFrom('list_item') + .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') + .where('list_mute.mutedByDid', '=', viewer) + .whereRef('list_item.subjectDid', '=', ref('did_handle.did')) + .select('list_item.listUri') + .limit(1) + .as('requesterMutedByList'), ]) - const [profileInfos, labels, listMutes] = await Promise.all([ + const [profileInfos, labels] = await Promise.all([ profileInfosQb.execute(), - this.services.label(this.db).getLabelsForSubjects(dids), - this.getListMutes(dids, viewer), + this.services.label.getLabelsForSubjects(dids), ]) const profileInfoByDid = profileInfos.reduce((acc, info) => { return Object.assign(acc, { [info.did]: info }) }, {} as Record>) + const listUris: string[] = profileInfos + .map((a) => a.requesterMutedByList) + .filter((list) => !!list) + const listViews = await this.services.graph.getListViews(listUris, viewer) + const views = results.map((result) => { const profileInfo = profileInfoByDid[result.did] const avatar = profileInfo?.avatarCid @@ -206,8 +242,14 @@ export class ActorViews { avatar, indexedAt: profileInfo?.indexedAt || undefined, viewer: { - muted: !!profileInfo?.requesterMuted || !!listMutes[result.did], - mutedByList: listMutes[result.did], + muted: + !!profileInfo?.requesterMuted || + !!profileInfo?.requesterMutedByList, + mutedByList: profileInfo.requesterMutedByList + ? this.services.graph.formatListViewBasic( + listViews[profileInfo.requesterMutedByList], + ) + : undefined, blockedBy: !!profileInfo.requesterBlockedBy, blocking: profileInfo.requesterBlocking || undefined, following: profileInfo?.requesterFollowing || undefined, @@ -245,41 +287,6 @@ export class ActorViews { return Array.isArray(result) ? views : views[0] } - - async getListMutes( - subjects: string[], - mutedBy: string, - ): Promise> { - if (subjects.length < 1) return {} - const res = await this.db.db - .selectFrom('list_item') - .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') - .innerJoin('list', 'list.uri', 'list_item.listUri') - .where('list_mute.mutedByDid', '=', mutedBy) - .where('list_item.subjectDid', 'in', subjects) - .selectAll('list') - .select('list_item.subjectDid as subjectDid') - .execute() - return res.reduce( - (acc, cur) => ({ - ...acc, - [cur.subjectDid]: { - uri: cur.uri, - cid: cur.cid, - name: cur.name, - purpose: cur.purpose, - avatar: cur.avatarCid - ? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid) - : undefined, - viewer: { - muted: true, - }, - indexedAt: cur.indexedAt, - }, - }), - {} as Record, - ) - } } type ActorResult = DidHandle diff --git a/packages/pds/src/app-view/services/feed/index.ts b/packages/pds/src/app-view/services/feed/index.ts index 326588efe36..732ba6623b3 100644 --- a/packages/pds/src/app-view/services/feed/index.ts +++ b/packages/pds/src/app-view/services/feed/index.ts @@ -1,6 +1,7 @@ import { sql } from 'kysely' import { AtUri } from '@atproto/uri' import { dedupeStrs } from '@atproto/common' +import { cborToLexRecord } from '@atproto/repo' import Database from '../../../db' import { countAll, notSoftDeletedClause } from '../../../db/util' import { ImageUriBuilder } from '../../../image/uri' @@ -28,21 +29,25 @@ import { LabelService, Labels } from '../label' import { ActorService } from '../actor' import { GraphService } from '../graph' import { FeedViews } from './views' -import { cborToLexRecord } from '@atproto/repo' +import { LabelCache } from '../../../label-cache' export * from './types' export class FeedService { - constructor(public db: Database, public imgUriBuilder: ImageUriBuilder) {} + constructor( + public db: Database, + public imgUriBuilder: ImageUriBuilder, + public labelCache: LabelCache, + ) {} - static creator(imgUriBuilder: ImageUriBuilder) { - return (db: Database) => new FeedService(db, imgUriBuilder) + static creator(imgUriBuilder: ImageUriBuilder, labelCache: LabelCache) { + return (db: Database) => new FeedService(db, imgUriBuilder, labelCache) } views = new FeedViews(this.db, this.imgUriBuilder) services = { - label: LabelService.creator()(this.db), - actor: ActorService.creator(this.imgUriBuilder)(this.db), + label: LabelService.creator(this.labelCache)(this.db), + actor: ActorService.creator(this.imgUriBuilder, this.labelCache)(this.db), graph: GraphService.creator(this.imgUriBuilder)(this.db), } @@ -114,7 +119,7 @@ export class FeedService { if (dids.length < 1) return {} const { ref } = this.db.db.dynamic const { skipLabels = false, includeSoftDeleted = false } = opts ?? {} - const [actors, labels, listMutes] = await Promise.all([ + const [actors, labels] = await Promise.all([ this.db.db .selectFrom('did_handle') .where('did_handle.did', 'in', dids) @@ -160,11 +165,25 @@ export class FeedService { .where('mutedByDid', '=', requester) .select('did') .as('requesterMuted'), + this.db.db + .selectFrom('list_item') + .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') + .where('list_mute.mutedByDid', '=', requester) + .whereRef('list_item.subjectDid', '=', ref('did_handle.did')) + .select('list_item.listUri') + .limit(1) + .as('requesterMutedByList'), ]) .execute(), this.services.label.getLabelsForSubjects(skipLabels ? [] : dids), - this.services.actor.views.getListMutes(dids, requester), ]) + const listUris: string[] = actors + .map((a) => a.requesterMutedByList) + .filter((list) => !!list) + const listViews = await this.services.graph.getListViews( + listUris, + requester, + ) return actors.reduce((acc, cur) => { const actorLabels = labels[cur.did] ?? [] return { @@ -177,8 +196,12 @@ export class FeedService { ? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid) : undefined, viewer: { - muted: !!cur?.requesterMuted || !!listMutes[cur.did], - mutedByList: listMutes[cur.did], + muted: !!cur?.requesterMuted || !!cur?.requesterMutedByList, + mutedByList: cur.requesterMutedByList + ? this.services.graph.formatListViewBasic( + listViews[cur.requesterMutedByList], + ) + : undefined, blockedBy: !!cur?.requesterBlockedBy, blocking: cur?.requesterBlocking || undefined, following: cur?.requesterFollowing || undefined, diff --git a/packages/pds/src/app-view/services/graph/index.ts b/packages/pds/src/app-view/services/graph/index.ts index bded56aeed4..94f7c58f54d 100644 --- a/packages/pds/src/app-view/services/graph/index.ts +++ b/packages/pds/src/app-view/services/graph/index.ts @@ -132,6 +132,22 @@ export class GraphService { }, } } + + formatListViewBasic(list: ListInfo) { + return { + uri: list.uri, + cid: list.cid, + name: list.name, + purpose: list.purpose, + avatar: list.avatarCid + ? this.imgUriBuilder.getCommonSignedUri('avatar', list.avatarCid) + : undefined, + indexedAt: list.indexedAt, + viewer: { + muted: !!list.viewerMuted, + }, + } + } } type ListInfo = List & { diff --git a/packages/pds/src/app-view/services/label/index.ts b/packages/pds/src/app-view/services/label/index.ts index 9de4670c16e..aa00ca29f95 100644 --- a/packages/pds/src/app-view/services/label/index.ts +++ b/packages/pds/src/app-view/services/label/index.ts @@ -3,14 +3,15 @@ import Database from '../../../db' import { Label } from '../../../lexicon/types/com/atproto/label/defs' import { ids } from '../../../lexicon/lexicons' import { sql } from 'kysely' +import { LabelCache } from '../../../label-cache' export type Labels = Record export class LabelService { - constructor(public db: Database) {} + constructor(public db: Database, public cache: LabelCache) {} - static creator() { - return (db: Database) => new LabelService(db) + static creator(cache: LabelCache) { + return (db: Database) => new LabelService(db, cache) } async formatAndCreate( @@ -63,14 +64,17 @@ export class LabelService { async getLabelsForUris( subjects: string[], includeNeg?: boolean, + skipCache?: boolean, ): Promise { if (subjects.length < 1) return {} - const res = await this.db.db - .selectFrom('label') - .where('label.uri', 'in', subjects) - .if(!includeNeg, (qb) => qb.where('neg', '=', 0)) - .selectAll() - .execute() + const res = skipCache + ? await this.db.db + .selectFrom('label') + .where('label.uri', 'in', subjects) + .if(!includeNeg, (qb) => qb.where('neg', '=', 0)) + .selectAll() + .execute() + : this.cache.forSubjects(subjects, includeNeg) return res.reduce((acc, cur) => { acc[cur.uri] ??= [] acc[cur.uri].push({ @@ -86,6 +90,7 @@ export class LabelService { async getLabelsForSubjects( subjects: string[], includeNeg?: boolean, + skipCache?: boolean, ): Promise { if (subjects.length < 1) return {} const expandedSubjects = subjects.flatMap((subject) => { @@ -97,7 +102,11 @@ export class LabelService { } return subject }) - const labels = await this.getLabelsForUris(expandedSubjects, includeNeg) + const labels = await this.getLabelsForUris( + expandedSubjects, + includeNeg, + skipCache, + ) return Object.keys(labels).reduce((acc, cur) => { const uri = cur.startsWith('at://') ? new AtUri(cur) : null if ( @@ -116,8 +125,12 @@ export class LabelService { }, {} as Labels) } - async getLabels(subject: string, includeNeg?: boolean): Promise { - const labels = await this.getLabelsForUris([subject], includeNeg) + async getLabels( + subject: string, + includeNeg?: boolean, + skipCache?: boolean, + ): Promise { + const labels = await this.getLabelsForUris([subject], includeNeg, skipCache) return labels[subject] ?? [] } diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 1e3ce8167d4..093edb1ba84 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -18,6 +18,7 @@ import { BackgroundQueue } from './event-stream/background-queue' import DidSqlCache from './did-cache' import { MountedAlgos } from './feed-gen/types' import { Crawlers } from './crawlers' +import { LabelCache } from './label-cache' export class AppContext { private _appviewAgent: AtpAgent | null @@ -39,6 +40,7 @@ export class AppContext { sequencer: Sequencer sequencerLeader: SequencerLeader labeler: Labeler + labelCache: LabelCache backgroundQueue: BackgroundQueue crawlers: Crawlers algos: MountedAlgos @@ -131,6 +133,10 @@ export class AppContext { return this.opts.labeler } + get labelCache(): LabelCache { + return this.opts.labelCache + } + get backgroundQueue(): BackgroundQueue { return this.opts.backgroundQueue } diff --git a/packages/pds/src/feed-gen/with-friends.ts b/packages/pds/src/feed-gen/with-friends.ts index a4a40364563..682387a5e61 100644 --- a/packages/pds/src/feed-gen/with-friends.ts +++ b/packages/pds/src/feed-gen/with-friends.ts @@ -12,39 +12,56 @@ const handler: AlgoHandler = async ( params: SkeletonParams, requester: string, ): Promise => { - const { cursor, limit = 50 } = params - const accountService = ctx.services.account(ctx.db) - const feedService = ctx.services.appView.feed(ctx.db) - const graphService = ctx.services.appView.graph(ctx.db) + // Temporary change to only return a post notifying users that the feed is down + return { + feedItems: [ + { + type: 'post', + uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jzinucnmbi2c', + cid: 'bafyreifmtn55tubbv7tefrq277nzfy4zu7ioithky276aho5ehb6w3nu6q', + postUri: + 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jzinucnmbi2c', + postAuthorDid: 'did:plc:z72i7hdynmk6r22z27h6tvur', + originatorDid: 'did:plc:z72i7hdynmk6r22z27h6tvur', + replyParent: null, + replyRoot: null, + sortAt: '2023-07-01T23:04:27.853Z', + }, + ], + } + // const { cursor, limit = 50 } = params + // const accountService = ctx.services.account(ctx.db) + // const feedService = ctx.services.appView.feed(ctx.db) + // const graphService = ctx.services.appView.graph(ctx.db) - const { ref } = ctx.db.db.dynamic + // const { ref } = ctx.db.db.dynamic - const keyset = new FeedKeyset(ref('post.indexedAt'), ref('post.cid')) - const sortFrom = keyset.unpack(cursor)?.primary + // const keyset = new FeedKeyset(ref('post.indexedAt'), ref('post.cid')) + // const sortFrom = keyset.unpack(cursor)?.primary - let postsQb = feedService - .selectPostQb() - .innerJoin('post_agg', 'post_agg.uri', 'post.uri') - .where('post_agg.likeCount', '>=', 5) - .whereExists((qb) => - qb - .selectFrom('follow') - .where('follow.creator', '=', requester) - .whereRef('follow.subjectDid', '=', 'post.creator'), - ) - .where((qb) => - accountService.whereNotMuted(qb, requester, [ref('post.creator')]), - ) - .whereNotExists(graphService.blockQb(requester, [ref('post.creator')])) - .where('post.indexedAt', '>', getFeedDateThreshold(sortFrom)) + // let postsQb = feedService + // .selectPostQb() + // // .innerJoin('post_agg', 'post_agg.uri', 'post.uri') + // // .where('post_agg.likeCount', '>=', 6) + // .whereExists((qb) => + // qb + // .selectFrom('follow') + // .where('follow.creator', '=', requester) + // .whereRef('follow.subjectDid', '=', 'post.creator'), + // ) + // .where((qb) => + // accountService.whereNotMuted(qb, requester, [ref('post.creator')]), + // ) + // .whereNotExists(graphService.blockQb(requester, [ref('post.creator')])) + // .where('post.indexedAt', '>', getFeedDateThreshold(sortFrom)) - postsQb = paginate(postsQb, { limit, cursor, keyset, tryIndex: true }) + // postsQb = paginate(postsQb, { limit, cursor, keyset, tryIndex: true }) - const feedItems = await postsQb.execute() - return { - feedItems, - cursor: keyset.packFromResult(feedItems), - } + // const feedItems = await postsQb.execute() + // return { + // feedItems, + // cursor: keyset.packFromResult(feedItems), + // } } export default handler diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index f77b654a069..3449425f63b 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -41,6 +41,7 @@ import { BackgroundQueue } from './event-stream/background-queue' import DidSqlCache from './did-cache' import { MountedAlgos } from './feed-gen/types' import { Crawlers } from './crawlers' +import { LabelCache } from './label-cache' export type { ServerConfigValues } from './config' export { ServerConfig } from './config' @@ -176,6 +177,8 @@ export class PDS { }) } + const labelCache = new LabelCache(db) + const services = createServices({ repoSigningKey, messageDispatcher, @@ -183,6 +186,7 @@ export class PDS { imgUriBuilder, imgInvalidator, labeler, + labelCache, backgroundQueue, crawlers, }) @@ -200,6 +204,7 @@ export class PDS { sequencer, sequencerLeader, labeler, + labelCache, services, mailer, imgUriBuilder, @@ -266,6 +271,7 @@ export class PDS { this.ctx.sequencerLeader.run() await this.ctx.sequencer.start() await this.ctx.db.startListeningToChannels() + this.ctx.labelCache.start() const server = this.app.listen(this.ctx.cfg.port) this.server = server this.server.keepAliveTimeout = 90000 @@ -275,6 +281,7 @@ export class PDS { } async destroy(): Promise { + this.ctx.labelCache.stop() await this.ctx.sequencerLeader.destroy() await this.terminator?.terminate() await this.ctx.backgroundQueue.destroy() diff --git a/packages/pds/src/label-cache.ts b/packages/pds/src/label-cache.ts new file mode 100644 index 00000000000..e4f23daa599 --- /dev/null +++ b/packages/pds/src/label-cache.ts @@ -0,0 +1,90 @@ +import { wait } from '@atproto/common' +import Database from './db' +import { Label } from './db/tables/label' +import { labelerLogger as log } from './logger' + +export class LabelCache { + bySubject: Record = {} + latestLabel = '' + refreshes = 0 + + destroyed = false + + constructor(public db: Database) {} + + start() { + this.poll() + } + + async fullRefresh() { + const allLabels = await this.db.db.selectFrom('label').selectAll().execute() + this.wipeCache() + this.processLabels(allLabels) + } + + async partialRefresh() { + const labels = await this.db.db + .selectFrom('label') + .selectAll() + .where('cts', '>', this.latestLabel) + .execute() + this.processLabels(labels) + } + + async poll() { + try { + if (this.destroyed) return + if (this.refreshes >= 120) { + await this.fullRefresh() + this.refreshes = 0 + } else { + await this.partialRefresh() + this.refreshes++ + } + } catch (err) { + log.error( + { err, latestLabel: this.latestLabel, refreshes: this.refreshes }, + 'label cache failed to refresh', + ) + } + await wait(500) + this.poll() + } + + processLabels(labels: Label[]) { + for (const label of labels) { + if (label.cts > this.latestLabel) { + this.latestLabel = label.cts + } + this.bySubject[label.uri] ??= [] + this.bySubject[label.uri].push(label) + } + } + + wipeCache() { + this.bySubject = {} + } + + stop() { + this.destroyed = true + } + + forSubject(subject: string, includeNeg = false): Label[] { + const labels = this.bySubject[subject] ?? [] + return includeNeg ? labels : labels.filter((l) => l.neg === 0) + } + + forSubjects(subjects: string[], includeNeg?: boolean): Label[] { + let labels: Label[] = [] + const alreadyAdded = new Set() + for (const subject of subjects) { + if (alreadyAdded.has(subject)) { + continue + } + const subLabels = this.forSubject(subject, includeNeg) + labels = [...labels, ...subLabels] + alreadyAdded.add(subject) + } + return labels + } +} diff --git a/packages/pds/src/sequencer/outbox.ts b/packages/pds/src/sequencer/outbox.ts index 335cd33cc3c..f0f62421a3d 100644 --- a/packages/pds/src/sequencer/outbox.ts +++ b/packages/pds/src/sequencer/outbox.ts @@ -108,14 +108,14 @@ export class Outbox { const evts = await this.sequencer.requestSeqRange({ earliestTime: backfillTime, earliestSeq: this.lastSeen > -1 ? this.lastSeen : backfillCursor, - limit: 10, + limit: 100, }) for (const evt of evts) { yield evt } // if we're within 50 of the sequencer, we call it good & switch to cutover const seqCursor = this.sequencer.lastSeen ?? -1 - if (seqCursor - this.lastSeen < 10) break + if (seqCursor - this.lastSeen < 100) break if (evts.length < 1) break } } diff --git a/packages/pds/src/sequencer/sequencer-leader.ts b/packages/pds/src/sequencer/sequencer-leader.ts index c951fda0d25..32964243f2e 100644 --- a/packages/pds/src/sequencer/sequencer-leader.ts +++ b/packages/pds/src/sequencer/sequencer-leader.ts @@ -1,5 +1,5 @@ import { DisconnectError } from '@atproto/xrpc-server' -import { jitter, wait } from '@atproto/common' +import { chunkArray, jitter, wait } from '@atproto/common' import { Leader } from '../db/leader' import { seqLogger as log } from '../logger' import Database from '../db' @@ -112,12 +112,20 @@ export class SequencerLeader { async sequenceOutgoing() { const unsequenced = await this.getUnsequenced() - for (const row of unsequenced) { - await this.db.db - .updateTable('repo_seq') - .set({ seq: this.nextSeqVal() }) - .where('id', '=', row.id) - .execute() + const chunks = chunkArray(unsequenced, 2000) + for (const chunk of chunks) { + await this.db.transaction(async (dbTxn) => { + await Promise.all( + chunk.map(async (row) => { + await dbTxn.db + .updateTable('repo_seq') + .set({ seq: this.nextSeqVal() }) + .where('id', '=', row.id) + .execute() + await this.db.notify('outgoing_repo_seq') + }), + ) + }) await this.db.notify('outgoing_repo_seq') } } diff --git a/packages/pds/src/sequencer/sequencer.ts b/packages/pds/src/sequencer/sequencer.ts index 4ea3b4ac836..61bcfa0efa7 100644 --- a/packages/pds/src/sequencer/sequencer.ts +++ b/packages/pds/src/sequencer/sequencer.ts @@ -126,7 +126,7 @@ export class Sequencer extends (EventEmitter as new () => SequencerEmitter) { try { const evts = await this.requestSeqRange({ earliestSeq: this.lastSeen, - limit: 10, + limit: 50, }) if (evts.length > 0) { this.emit('events', evts) diff --git a/packages/pds/src/services/index.ts b/packages/pds/src/services/index.ts index dd23198a8fa..f89fd917082 100644 --- a/packages/pds/src/services/index.ts +++ b/packages/pds/src/services/index.ts @@ -17,6 +17,7 @@ import { Labeler } from '../labeler' import { LabelService } from '../app-view/services/label' import { BackgroundQueue } from '../event-stream/background-queue' import { Crawlers } from '../crawlers' +import { LabelCache } from '../label-cache' export function createServices(resources: { repoSigningKey: crypto.Keypair @@ -25,6 +26,7 @@ export function createServices(resources: { imgUriBuilder: ImageUriBuilder imgInvalidator: ImageInvalidator labeler: Labeler + labelCache: LabelCache backgroundQueue: BackgroundQueue crawlers: Crawlers }): Services { @@ -35,6 +37,7 @@ export function createServices(resources: { imgUriBuilder, imgInvalidator, labeler, + labelCache, backgroundQueue, crawlers, } = resources @@ -57,11 +60,11 @@ export function createServices(resources: { imgInvalidator, ), appView: { - actor: ActorService.creator(imgUriBuilder), + actor: ActorService.creator(imgUriBuilder, labelCache), graph: GraphService.creator(imgUriBuilder), - feed: FeedService.creator(imgUriBuilder), + feed: FeedService.creator(imgUriBuilder, labelCache), indexing: IndexingService.creator(backgroundQueue), - label: LabelService.creator(), + label: LabelService.creator(labelCache), }, } } diff --git a/packages/pds/src/services/repo/index.ts b/packages/pds/src/services/repo/index.ts index c41e01b4c18..7dc1dac4252 100644 --- a/packages/pds/src/services/repo/index.ts +++ b/packages/pds/src/services/repo/index.ts @@ -132,10 +132,12 @@ export class RepoService { toWrite: { did: string; writes: PreparedWrite[]; swapCommitCid?: CID }, times: number, timeout = 100, + prevStorage?: SqlRepoStorage, ) { this.db.assertNotTransaction() const { did, writes, swapCommitCid } = toWrite - const storage = new SqlRepoStorage(this.db, did) + // we may have some useful cached blocks in the storage, so re-use the previous instance + const storage = prevStorage ?? new SqlRepoStorage(this.db, did) const commit = await this.formatCommit(storage, did, writes, swapCommitCid) try { await this.serviceTx(async (srvcTx) => @@ -147,7 +149,7 @@ export class RepoService { throw err } await wait(timeout) - return this.processWrites(toWrite, times - 1, timeout) + return this.processWrites(toWrite, times - 1, timeout, storage) } else { throw err } @@ -169,6 +171,8 @@ export class RepoService { if (swapCommit && !currRoot.equals(swapCommit)) { throw new BadCommitSwapError(currRoot) } + // cache last commit since there's likely overlap + await storage.cacheCommit(currRoot) const recordTxn = this.services.record(this.db) for (const write of writes) { const { action, uri, swapCid } = write diff --git a/packages/pds/src/sql-repo-storage.ts b/packages/pds/src/sql-repo-storage.ts index 3002ebe6ecf..0b9928c0371 100644 --- a/packages/pds/src/sql-repo-storage.ts +++ b/packages/pds/src/sql-repo-storage.ts @@ -42,6 +42,24 @@ export class SqlRepoStorage extends RepoStorage { return CID.parse(res.root) } + // proactively cache all blocks from a particular commit (to prevent multiple roundtrips) + async cacheCommit(cid: CID): Promise { + const res = await this.db.db + .selectFrom('repo_commit_block') + .innerJoin('ipld_block', (join) => + join + .onRef('ipld_block.cid', '=', 'repo_commit_block.block') + .onRef('ipld_block.creator', '=', 'repo_commit_block.creator'), + ) + .where('repo_commit_block.creator', '=', this.did) + .where('repo_commit_block.commit', '=', cid.toString()) + .select(['ipld_block.cid', 'ipld_block.content']) + .execute() + for (const row of res) { + this.cache.set(CID.parse(row.cid), row.content) + } + } + async getBytes(cid: CID): Promise { const cached = this.cache.get(cid) if (cached) return cached diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index 225b3a85a1d..115d62a4a9f 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -25,6 +25,7 @@ export type TestServerInfo = { url: string ctx: AppContext close: CloseFn + processAll: () => Promise } export type TestServerOpts = { @@ -154,6 +155,9 @@ export const runTestServer = async ( const pdsServer = await pds.start() const pdsPort = (pdsServer.address() as AddressInfo).port + // we refresh label cache by hand in `processAll` instead of on a timer + pds.ctx.labelCache.stop() + return { url: `http://localhost:${pdsPort}`, ctx: pds.ctx, @@ -161,6 +165,10 @@ export const runTestServer = async ( await pds.destroy() await plcServer.destroy() }, + processAll: async () => { + await pds.ctx.backgroundQueue.processAll() + await pds.ctx.labelCache.fullRefresh() + }, } } diff --git a/packages/pds/tests/account-deletion.test.ts b/packages/pds/tests/account-deletion.test.ts index 00004d78d7e..6720b52aee4 100644 --- a/packages/pds/tests/account-deletion.test.ts +++ b/packages/pds/tests/account-deletion.test.ts @@ -146,7 +146,7 @@ describe('account deletion', () => { did: carol.did, password: carol.password, }) - await server.ctx.backgroundQueue.processAll() // Finish background hard-deletions + await server.processAll() // Finish background hard-deletions }) it('no longer lets the user log in', async () => { diff --git a/packages/pds/tests/algos/hot-classic.test.ts b/packages/pds/tests/algos/hot-classic.test.ts index 180c4c90b92..1d5804f689a 100644 --- a/packages/pds/tests/algos/hot-classic.test.ts +++ b/packages/pds/tests/algos/hot-classic.test.ts @@ -35,7 +35,7 @@ describe('algo hot-classic', () => { alice = sc.dids.alice bob = sc.dids.bob - await server.ctx.backgroundQueue.processAll() + await server.processAll() }) afterAll(async () => { @@ -63,7 +63,7 @@ describe('algo hot-classic', () => { await sc.like(sc.dids[name], two.ref) await sc.like(sc.dids[name], three.ref) } - await server.ctx.backgroundQueue.processAll() + await server.processAll() const res = await agent.api.app.bsky.feed.getFeed( { feed: feedUri }, diff --git a/packages/pds/tests/algos/whats-hot.test.ts b/packages/pds/tests/algos/whats-hot.test.ts index d26c73dcf62..08d9f1a1b82 100644 --- a/packages/pds/tests/algos/whats-hot.test.ts +++ b/packages/pds/tests/algos/whats-hot.test.ts @@ -38,7 +38,7 @@ describe('algo whats-hot', () => { alice = sc.dids.alice bob = sc.dids.bob carol = sc.dids.carol - await server.ctx.backgroundQueue.processAll() + await server.processAll() }) afterAll(async () => { @@ -76,7 +76,7 @@ describe('algo whats-hot', () => { await sc.like(sc.dids[name], five.ref) } } - await server.ctx.backgroundQueue.processAll() + await server.processAll() // move the 3rd post 5 hours into the past to check gravity await server.ctx.db.db diff --git a/packages/pds/tests/algos/with-friends.test.ts b/packages/pds/tests/algos/with-friends.test.ts index 510274242cd..7e6e79357d9 100644 --- a/packages/pds/tests/algos/with-friends.test.ts +++ b/packages/pds/tests/algos/with-friends.test.ts @@ -40,7 +40,7 @@ describe.skip('algo with friends', () => { carol = sc.dids.carol dan = sc.dids.dan - await server.ctx.backgroundQueue.processAll() + await server.processAll() }) afterAll(async () => { diff --git a/packages/pds/tests/blob-deletes.test.ts b/packages/pds/tests/blob-deletes.test.ts index bf902b57169..aa8122423ba 100644 --- a/packages/pds/tests/blob-deletes.test.ts +++ b/packages/pds/tests/blob-deletes.test.ts @@ -58,7 +58,7 @@ describe('blob deletes', () => { ) const post = await sc.post(alice, 'test', undefined, [img]) await sc.deletePost(alice, post.ref.uri) - await server.ctx.backgroundQueue.processAll() + await server.processAll() const dbBlobs = await getDbBlobsForDid(alice) expect(dbBlobs.length).toBe(0) @@ -80,7 +80,7 @@ describe('blob deletes', () => { ) await updateProfile(sc, alice, img.image, img.image) await updateProfile(sc, alice, img2.image, img2.image) - await server.ctx.backgroundQueue.processAll() + await server.processAll() const dbBlobs = await getDbBlobsForDid(alice) expect(dbBlobs.length).toBe(1) @@ -109,7 +109,7 @@ describe('blob deletes', () => { ) await updateProfile(sc, alice, img.image, img.image) await updateProfile(sc, alice, img.image, img2.image) - await server.ctx.backgroundQueue.processAll() + await server.processAll() const dbBlobs = await getDbBlobsForDid(alice) expect(dbBlobs.length).toBe(2) @@ -160,7 +160,7 @@ describe('blob deletes', () => { }, { encoding: 'application/json', headers: sc.getHeaders(alice) }, ) - await server.ctx.backgroundQueue.processAll() + await server.processAll() const dbBlobs = await getDbBlobsForDid(alice) expect(dbBlobs.length).toBe(1) diff --git a/packages/pds/tests/event-stream/sync.test.ts b/packages/pds/tests/event-stream/sync.test.ts index cf42f86223c..a596b7240ce 100644 --- a/packages/pds/tests/event-stream/sync.test.ts +++ b/packages/pds/tests/event-stream/sync.test.ts @@ -68,6 +68,7 @@ describe('sync', () => { services.repo(dbTxn).indexWrites(writes, now), ) } + await server.processAll() // Check indexed timeline const aliceTL = await agent.api.app.bsky.feed.getTimeline( {}, diff --git a/packages/pds/tests/feed-generation.test.ts b/packages/pds/tests/feed-generation.test.ts index e5c9ab8e9c2..caa1f38423c 100644 --- a/packages/pds/tests/feed-generation.test.ts +++ b/packages/pds/tests/feed-generation.test.ts @@ -38,7 +38,7 @@ describe('feed generation', () => { agent = network.pds.getClient() sc = new SeedClient(agent) await basicSeed(sc) - await network.pds.ctx.backgroundQueue.processAll() + await network.processAll() alice = sc.dids.alice const allUri = AtUri.make(alice, 'app.bsky.feed.generator', 'all') const feedUriBadPagination = AtUri.make( diff --git a/packages/pds/tests/indexing.test.ts b/packages/pds/tests/indexing.test.ts index 7402d7e6aa9..4aede170617 100644 --- a/packages/pds/tests/indexing.test.ts +++ b/packages/pds/tests/indexing.test.ts @@ -82,7 +82,7 @@ describe('indexing', () => { await services .repo(db) .processWrites({ did: sc.dids.alice, writes: [createRecord] }, 1) - await server.ctx.backgroundQueue.processAll() + await server.processAll() const getAfterCreate = await agent.api.app.bsky.feed.getPostThread( { uri: uri.toString() }, @@ -95,7 +95,7 @@ describe('indexing', () => { await services .repo(db) .processWrites({ did: sc.dids.alice, writes: [updateRecord] }, 1) - await server.ctx.backgroundQueue.processAll() + await server.processAll() const getAfterUpdate = await agent.api.app.bsky.feed.getPostThread( { uri: uri.toString() }, @@ -108,7 +108,7 @@ describe('indexing', () => { await services .repo(db) .processWrites({ did: sc.dids.alice, writes: [deleteRecord] }, 1) - await server.ctx.backgroundQueue.processAll() + await server.processAll() const getAfterDelete = agent.api.app.bsky.feed.getPostThread( { uri: uri.toString() }, @@ -157,7 +157,7 @@ describe('indexing', () => { await services .repo(db) .processWrites({ did: sc.dids.dan, writes: [createRecord] }, 1) - await server.ctx.backgroundQueue.processAll() + await server.processAll() const getAfterCreate = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.dan }, @@ -170,7 +170,7 @@ describe('indexing', () => { await services .repo(db) .processWrites({ did: sc.dids.dan, writes: [updateRecord] }, 1) - await server.ctx.backgroundQueue.processAll() + await server.processAll() const getAfterUpdate = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.dan }, @@ -183,7 +183,7 @@ describe('indexing', () => { await services .repo(db) .processWrites({ did: sc.dids.dan, writes: [deleteRecord] }, 1) - await server.ctx.backgroundQueue.processAll() + await server.processAll() const getAfterDelete = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.dan }, diff --git a/packages/pds/tests/labeler/labeler.test.ts b/packages/pds/tests/labeler/labeler.test.ts index ea943dcf46d..2525732357a 100644 --- a/packages/pds/tests/labeler/labeler.test.ts +++ b/packages/pds/tests/labeler/labeler.test.ts @@ -1,6 +1,6 @@ import { AtUri, BlobRef } from '@atproto/api' import stream from 'stream' -import { runTestServer, CloseFn } from '../_util' +import { runTestServer, CloseFn, TestServerInfo } from '../_util' import { Labeler } from '../../src/labeler' import { AppContext, Database } from '../../src' import { BlobStore, cidForRecord } from '@atproto/repo' @@ -11,6 +11,7 @@ import { LabelService } from '../../src/app-view/services/label' import { BackgroundQueue } from '../../src/event-stream/background-queue' describe('labeler', () => { + let server: TestServerInfo let close: CloseFn let labeler: Labeler let labelSrvc: LabelService @@ -21,7 +22,7 @@ describe('labeler', () => { let goodBlob: BlobRef beforeAll(async () => { - const server = await runTestServer({ + server = await runTestServer({ dbPostgresSchema: 'views_author_feed', }) close = server.close @@ -63,6 +64,7 @@ describe('labeler', () => { const uri = postUri() labeler.processRecord(uri, post) await labeler.processAll() + await server.processAll() const labels = await labelSrvc.getLabels(uri.toString()) expect(labels.length).toBe(1) expect(labels[0]).toMatchObject({ @@ -100,6 +102,7 @@ describe('labeler', () => { const uri = postUri() labeler.processRecord(uri, post) await labeler.processAll() + await server.processAll() const dbLabels = await labelSrvc.getLabels(uri.toString()) const labels = dbLabels.map((row) => row.val).sort() expect(labels).toEqual( @@ -119,6 +122,7 @@ describe('labeler', () => { cts: new Date().toISOString(), }) .execute() + await server.processAll() const labels = await labelSrvc.getLabelsForProfile('did:example:alice') // 4 from earlier & then just added one diff --git a/packages/pds/tests/migrations/blob-creator.test.ts b/packages/pds/tests/migrations/blob-creator.test.ts index 8d6843a1136..defe3bde6f4 100644 --- a/packages/pds/tests/migrations/blob-creator.test.ts +++ b/packages/pds/tests/migrations/blob-creator.test.ts @@ -4,7 +4,7 @@ import { cidForCbor, TID } from '@atproto/common' import { Kysely } from 'kysely' import { AtUri } from '@atproto/uri' -describe('blob creator migration', () => { +describe.skip('blob creator migration', () => { let db: Database let rawDb: Kysely diff --git a/packages/pds/tests/migrations/indexed-at-on-record.test.ts b/packages/pds/tests/migrations/indexed-at-on-record.test.ts index d2a3f8f4f8f..02147816203 100644 --- a/packages/pds/tests/migrations/indexed-at-on-record.test.ts +++ b/packages/pds/tests/migrations/indexed-at-on-record.test.ts @@ -4,7 +4,7 @@ import { dataToCborBlock, TID } from '@atproto/common' import { AtUri } from '@atproto/uri' import { Kysely } from 'kysely' -describe('indexedAt on record migration', () => { +describe.skip('indexedAt on record migration', () => { let db: Database let rawDb: Kysely diff --git a/packages/pds/tests/migrations/post-hierarchy.test.ts b/packages/pds/tests/migrations/post-hierarchy.test.ts index 145be7bb16c..3e4a0a4ff3d 100644 --- a/packages/pds/tests/migrations/post-hierarchy.test.ts +++ b/packages/pds/tests/migrations/post-hierarchy.test.ts @@ -5,7 +5,7 @@ import usersSeed from '../seeds/users' import threadSeed, { walk, item, Item } from '../seeds/thread' import { CloseFn, runTestServer } from '../_util' -describe('post hierarchy migration', () => { +describe.skip('post hierarchy migration', () => { let db: Database let close: CloseFn let sc: SeedClient diff --git a/packages/pds/tests/migrations/repo-sync-data-pt2.test.ts b/packages/pds/tests/migrations/repo-sync-data-pt2.test.ts index ad046836ed6..91971fb7043 100644 --- a/packages/pds/tests/migrations/repo-sync-data-pt2.test.ts +++ b/packages/pds/tests/migrations/repo-sync-data-pt2.test.ts @@ -5,7 +5,7 @@ import { CloseFn, runTestServer } from '../_util' import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' -describe('repo sync data migration', () => { +describe.skip('repo sync data migration', () => { let db: Database let rawDb: Kysely let close: CloseFn diff --git a/packages/pds/tests/migrations/repo-sync-data.test.ts b/packages/pds/tests/migrations/repo-sync-data.test.ts index 4b56aede9db..2fd17c199eb 100644 --- a/packages/pds/tests/migrations/repo-sync-data.test.ts +++ b/packages/pds/tests/migrations/repo-sync-data.test.ts @@ -5,7 +5,7 @@ import { TID } from '@atproto/common' import { Kysely } from 'kysely' import { CID } from 'multiformats/cid' -describe('repo sync data migration', () => { +describe.skip('repo sync data migration', () => { let db: Database let rawDb: Kysely let memoryStore: MemoryBlockstore diff --git a/packages/pds/tests/migrations/user-partitioned-cids.test.ts b/packages/pds/tests/migrations/user-partitioned-cids.test.ts index 048ccd9dacf..e6eff220445 100644 --- a/packages/pds/tests/migrations/user-partitioned-cids.test.ts +++ b/packages/pds/tests/migrations/user-partitioned-cids.test.ts @@ -5,7 +5,7 @@ import { Kysely } from 'kysely' import { Block } from 'multiformats/block' import * as uint8arrays from 'uint8arrays' -describe('user partitioned cids migration', () => { +describe.skip('user partitioned cids migration', () => { let db: Database let rawDb: Kysely diff --git a/packages/pds/tests/migrations/user-table-did-pkey.test.ts b/packages/pds/tests/migrations/user-table-did-pkey.test.ts index bda821a1539..881907e71b8 100644 --- a/packages/pds/tests/migrations/user-table-did-pkey.test.ts +++ b/packages/pds/tests/migrations/user-table-did-pkey.test.ts @@ -2,7 +2,7 @@ import { Database } from '../../src' import { randomStr } from '@atproto/crypto' import { Kysely } from 'kysely' -describe('user table did pkey migration', () => { +describe.skip('user table did pkey migration', () => { let db: Database let rawDb: Kysely diff --git a/packages/pds/tests/moderation.test.ts b/packages/pds/tests/moderation.test.ts index caf645b3c9d..19bcbee1ad3 100644 --- a/packages/pds/tests/moderation.test.ts +++ b/packages/pds/tests/moderation.test.ts @@ -292,7 +292,7 @@ describe('moderation', () => { password: 'password', token: deletionToken, }) - await server.ctx.backgroundQueue.processAll() + await server.processAll() // Take action on deleted content const { data: action } = await agent.api.com.atproto.admin.takeModerationAction( diff --git a/packages/pds/tests/proxied/feedgen.test.ts b/packages/pds/tests/proxied/feedgen.test.ts index 2914813750d..44a89e0b34c 100644 --- a/packages/pds/tests/proxied/feedgen.test.ts +++ b/packages/pds/tests/proxied/feedgen.test.ts @@ -36,7 +36,6 @@ describe('feedgen proxy view', () => { sc.getHeaders(sc.dids.alice), ) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() // mock getFeedGenerator() for use by pds's getFeed since we don't have a proper feedGenDid or feed publisher FeedNS.prototype.getFeedGenerator = async function (params, opts) { if (params?.feed === feedUri.toString()) { diff --git a/packages/pds/tests/views/actor-search.test.ts b/packages/pds/tests/views/actor-search.test.ts index 5cb778db4a7..f778f1387c4 100644 --- a/packages/pds/tests/views/actor-search.test.ts +++ b/packages/pds/tests/views/actor-search.test.ts @@ -28,7 +28,7 @@ describe('pds user search views', () => { sc = new SeedClient(agent) await usersBulkSeed(sc) headers = sc.getHeaders(Object.values(sc.dids)[0]) - await server.ctx.backgroundQueue.processAll() + await server.processAll() }) afterAll(async () => { diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index 68cdd0bc48b..64246f0fcf2 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -33,7 +33,7 @@ describe('pds author feed views', () => { bob = sc.dids.bob carol = sc.dids.carol dan = sc.dids.dan - await server.ctx.backgroundQueue.processAll() + await server.processAll() }) afterAll(async () => { diff --git a/packages/pds/tests/views/blocks.test.ts b/packages/pds/tests/views/blocks.test.ts index 4d72e639b1d..a3815a6a668 100644 --- a/packages/pds/tests/views/blocks.test.ts +++ b/packages/pds/tests/views/blocks.test.ts @@ -43,7 +43,7 @@ describe('pds views with blocking', () => { sc.posts[dan][0].ref, 'alice replies to dan', ) - await server.ctx.backgroundQueue.processAll() + await server.processAll() }) afterAll(async () => { diff --git a/packages/pds/tests/views/follows.test.ts b/packages/pds/tests/views/follows.test.ts index 982aa2c0d29..606b85f9d48 100644 --- a/packages/pds/tests/views/follows.test.ts +++ b/packages/pds/tests/views/follows.test.ts @@ -27,7 +27,7 @@ describe('pds follow views', () => { sc = new SeedClient(agent) await followsSeed(sc) alice = sc.dids.alice - await server.ctx.backgroundQueue.processAll() + await server.processAll() }) afterAll(async () => { diff --git a/packages/pds/tests/views/likes.test.ts b/packages/pds/tests/views/likes.test.ts index 10bef798aba..d77251d8b25 100644 --- a/packages/pds/tests/views/likes.test.ts +++ b/packages/pds/tests/views/likes.test.ts @@ -28,7 +28,7 @@ describe('pds like views', () => { await likesSeed(sc) alice = sc.dids.alice bob = sc.dids.bob - await server.ctx.backgroundQueue.processAll() + await server.processAll() }) afterAll(async () => { diff --git a/packages/pds/tests/views/mute-lists.test.ts b/packages/pds/tests/views/mute-lists.test.ts index 0cf15770617..64692a81b24 100644 --- a/packages/pds/tests/views/mute-lists.test.ts +++ b/packages/pds/tests/views/mute-lists.test.ts @@ -30,7 +30,7 @@ describe('pds views with mutes from mute lists', () => { // add follows to ensure mutes work even w follows await sc.follow(carol, dan) await sc.follow(dan, carol) - await server.ctx.backgroundQueue.processAll() + await server.processAll() }) afterAll(async () => { diff --git a/packages/pds/tests/views/mutes.test.ts b/packages/pds/tests/views/mutes.test.ts index a3a17eff595..ac32d43ab6f 100644 --- a/packages/pds/tests/views/mutes.test.ts +++ b/packages/pds/tests/views/mutes.test.ts @@ -32,7 +32,7 @@ describe('mute views', () => { { headers: sc.getHeaders(silas), encoding: 'application/json' }, ) } - await server.ctx.backgroundQueue.processAll() + await server.processAll() }) afterAll(async () => { diff --git a/packages/pds/tests/views/notifications.test.ts b/packages/pds/tests/views/notifications.test.ts index 67b00331e3c..b5d9907140c 100644 --- a/packages/pds/tests/views/notifications.test.ts +++ b/packages/pds/tests/views/notifications.test.ts @@ -33,7 +33,7 @@ describe('pds notification views', () => { sc = new SeedClient(agent) await basicSeed(sc) alice = sc.dids.alice - await server.ctx.backgroundQueue.processAll() + await server.processAll() }) afterAll(async () => { @@ -75,7 +75,7 @@ describe('pds notification views', () => { sc.replies[alice][0].ref, 'indeed', ) - await server.ctx.backgroundQueue.processAll() + await server.processAll() const notifCountAlice = await agent.api.app.bsky.notification.getUnreadCount( diff --git a/packages/pds/tests/views/popular.test.ts b/packages/pds/tests/views/popular.test.ts index 1456f4cbc33..ca21ee31f4f 100644 --- a/packages/pds/tests/views/popular.test.ts +++ b/packages/pds/tests/views/popular.test.ts @@ -54,7 +54,7 @@ describe('popular views', () => { alice = sc.dids.alice bob = sc.dids.bob - await server.ctx.backgroundQueue.processAll() + await server.processAll() }) afterAll(async () => { @@ -82,7 +82,7 @@ describe('popular views', () => { await sc.like(sc.dids[name], two.ref) await sc.like(sc.dids[name], three.ref) } - await server.ctx.backgroundQueue.processAll() + await server.processAll() const res = await agent.api.app.bsky.unspecced.getPopular( {}, diff --git a/packages/pds/tests/views/posts.test.ts b/packages/pds/tests/views/posts.test.ts index 55b2312cf12..7bf6e6919cf 100644 --- a/packages/pds/tests/views/posts.test.ts +++ b/packages/pds/tests/views/posts.test.ts @@ -15,7 +15,7 @@ describe('pds posts views', () => { agent = new AtpAgent({ service: server.url }) sc = new SeedClient(agent) await basicSeed(sc) - await server.ctx.labeler.processAll() + await server.processAll() }) afterAll(async () => { diff --git a/packages/pds/tests/views/profile.test.ts b/packages/pds/tests/views/profile.test.ts index 1f68946811d..b3f5520a8b9 100644 --- a/packages/pds/tests/views/profile.test.ts +++ b/packages/pds/tests/views/profile.test.ts @@ -27,7 +27,7 @@ describe('pds profile views', () => { alice = sc.dids.alice bob = sc.dids.bob dan = sc.dids.dan - await server.ctx.backgroundQueue.processAll() + await server.processAll() }) afterAll(async () => { diff --git a/packages/pds/tests/views/reposts.test.ts b/packages/pds/tests/views/reposts.test.ts index 5e4eb6e0b37..1c61215e3e5 100644 --- a/packages/pds/tests/views/reposts.test.ts +++ b/packages/pds/tests/views/reposts.test.ts @@ -22,7 +22,7 @@ describe('pds repost views', () => { await repostsSeed(sc) alice = sc.dids.alice bob = sc.dids.bob - await server.ctx.backgroundQueue.processAll() + await server.processAll() }) afterAll(async () => { diff --git a/packages/pds/tests/views/suggestions.test.ts b/packages/pds/tests/views/suggestions.test.ts index 1c075fe17a8..4bec6dc3dd2 100644 --- a/packages/pds/tests/views/suggestions.test.ts +++ b/packages/pds/tests/views/suggestions.test.ts @@ -16,7 +16,7 @@ describe('pds user search views', () => { agent = new AtpAgent({ service: server.url }) sc = new SeedClient(agent) await basicSeed(sc) - await server.ctx.backgroundQueue.processAll() + await server.processAll() const suggestions = [ { did: sc.dids.bob, order: 1 }, diff --git a/packages/pds/tests/views/thread.test.ts b/packages/pds/tests/views/thread.test.ts index 0f4fa68b35e..8838ea20e44 100644 --- a/packages/pds/tests/views/thread.test.ts +++ b/packages/pds/tests/views/thread.test.ts @@ -33,6 +33,7 @@ describe('pds thread views', () => { agent = new AtpAgent({ service: server.url }) sc = new SeedClient(agent) await basicSeed(sc) + await server.processAll() alice = sc.dids.alice bob = sc.dids.bob carol = sc.dids.carol @@ -41,7 +42,7 @@ describe('pds thread views', () => { beforeAll(async () => { // Add a repost of a reply so that we can confirm viewer state in the thread await sc.repost(bob, sc.replies[alice][0].ref) - await server.ctx.backgroundQueue.processAll() + await server.processAll() }) afterAll(async () => { @@ -140,7 +141,7 @@ describe('pds thread views', () => { 'Reply reply', ) indexes.aliceReplyReply = sc.replies[alice].length - 1 - await server.ctx.backgroundQueue.processAll() + await server.processAll() const thread1 = await agent.api.app.bsky.feed.getPostThread( { uri: sc.posts[alice][indexes.aliceRoot].ref.uriStr }, @@ -149,7 +150,7 @@ describe('pds thread views', () => { expect(forSnapshot(thread1.data.thread)).toMatchSnapshot() await sc.deletePost(bob, sc.replies[bob][indexes.bobReply].ref.uri) - await server.ctx.backgroundQueue.processAll() + await server.processAll() const thread2 = await agent.api.app.bsky.feed.getPostThread( { uri: sc.posts[alice][indexes.aliceRoot].ref.uriStr }, diff --git a/packages/pds/tests/views/timeline.test.ts b/packages/pds/tests/views/timeline.test.ts index dc0d10a79cb..b55a96e691d 100644 --- a/packages/pds/tests/views/timeline.test.ts +++ b/packages/pds/tests/views/timeline.test.ts @@ -55,7 +55,7 @@ describe('timeline views', () => { labelPostB.cidStr, { create: ['kind'] }, ) - await server.ctx.backgroundQueue.processAll() + await server.processAll() }) afterAll(async () => { From 05a7c4619237cbd6b3577087d43646213fd88565 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 5 Jul 2023 17:22:53 -0500 Subject: [PATCH 023/237] Add migrations for handmade indices (#1266) * add indices * add record index to bsky * sync-up bsky index migration, remove duplicate index * backpressure on bsky backfill indexing (#1268) * backpressure on bsky backfill indexing * skip image resolution for text labeler * increase background queue concurrency for backfill * tidy * Proxy timeline skeleton construction (#1264) proxy timeline skeleton construction to appview * Only pass through known params on timeline skeleton (#1270) only pass through own params * Require headers on getRecord proxy (#1271) require headers on getRecord proxy * Add boolean for enabling generic appview proxying (#1273) * add boolean config for enabling generic proxying * tweak * tweak cfg var name * tweak * :bug: Only ignore reports for specific at-uri when ignoreSubject contains at-uri (#1251) * Move timeline construction to appview (#1274) full switch timeline construction to appview * Better propagate errors on repo streams (#1276) better propgate errors on repo streams * Log pds sequencer leader stats (#1277) log pds sequencer leader stats * Explicit dns servers (#1281) * add ability to setup explicit dns servers * cleanup * fix * reorder * pr feedback * Thread through id-resolver cfg (#1282) thread through id-resolver-cfg * Bsky log commit error (#1275) * don't bail on bad record index * add build * temporarily disable check, full reindex on rabase * don't bail on bad record index during rebase, track last commit on rebase * log bsky repo subscription stats * add running and waiting count to repo sub stats * re-enable fast path for happy rebases * only hold onto seq in cursor consecutivelist, don't hold onto whole completed messages * Misc scaling (#1284) * limit backsearch to 1 day instead of 3 * lower like count threshold * bump to 6 * disable like count check * disable with friends * preemptively cache last commit * inline list mutes * actor service * label cache * placehodler on popular with friends * bulk sequence * no limit but chunk * bump chunk to 5k * try 10k * fix notify * tweaking * syntax * one more fix * increase backfill allowance * full refresh label cache * limit 1 on mute list * reserve aclu handle * clean up testing with label cache * note on with-friends * rm defer from label cache * label cache error handling * rm branch build * build appview * update indices --------- Co-authored-by: Devin Ivy Co-authored-by: Foysal Ahamed --- .../workflows/build-and-push-bsky-aws.yaml | 1 - ...30703T045536691Z-feed-and-label-indices.ts | 31 +++++++++++++++++++ packages/bsky/src/db/migrations/index.ts | 1 + ...30703T044601833Z-feed-and-label-indices.ts | 19 ++++++++++++ packages/pds/src/db/migrations/index.ts | 1 + 5 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 packages/bsky/src/db/migrations/20230703T045536691Z-feed-and-label-indices.ts create mode 100644 packages/pds/src/db/migrations/20230703T044601833Z-feed-and-label-indices.ts diff --git a/.github/workflows/build-and-push-bsky-aws.yaml b/.github/workflows/build-and-push-bsky-aws.yaml index 9e5c2e9b870..b656dec89c9 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 - - bsky-log-commit-error env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} diff --git a/packages/bsky/src/db/migrations/20230703T045536691Z-feed-and-label-indices.ts b/packages/bsky/src/db/migrations/20230703T045536691Z-feed-and-label-indices.ts new file mode 100644 index 00000000000..bf54deeae2a --- /dev/null +++ b/packages/bsky/src/db/migrations/20230703T045536691Z-feed-and-label-indices.ts @@ -0,0 +1,31 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createIndex('label_cts_idx') + .on('label') + .column('cts') + .execute() + await db.schema.dropIndex('feed_item_originator_idx').execute() + await db.schema + .createIndex('feed_item_originator_cursor_idx') + .on('feed_item') + .columns(['originatorDid', 'sortAt', 'cid']) + .execute() + await db.schema + .createIndex('record_did_idx') + .on('record') + .column('did') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropIndex('label_cts_idx').execute() + await db.schema.dropIndex('feed_item_originator_cursor_idx').execute() + await db.schema + .createIndex('feed_item_originator_idx') + .on('feed_item') + .column('originatorDid') + .execute() + await db.schema.dropIndex('record_did_idx').execute() +} diff --git a/packages/bsky/src/db/migrations/index.ts b/packages/bsky/src/db/migrations/index.ts index 487e61b7cf5..6d29ffe6576 100644 --- a/packages/bsky/src/db/migrations/index.ts +++ b/packages/bsky/src/db/migrations/index.ts @@ -19,3 +19,4 @@ export * as _20230611T215300060Z from './20230611T215300060Z-actor-state' export * as _20230620T161134972Z from './20230620T161134972Z-post-langs' export * as _20230627T212437895Z from './20230627T212437895Z-optional-handle' export * as _20230629T220835893Z from './20230629T220835893Z-remove-post-hierarchy' +export * as _20230703T045536691Z from './20230703T045536691Z-feed-and-label-indices' diff --git a/packages/pds/src/db/migrations/20230703T044601833Z-feed-and-label-indices.ts b/packages/pds/src/db/migrations/20230703T044601833Z-feed-and-label-indices.ts new file mode 100644 index 00000000000..530b833c757 --- /dev/null +++ b/packages/pds/src/db/migrations/20230703T044601833Z-feed-and-label-indices.ts @@ -0,0 +1,19 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createIndex('label_cts_idx') + .on('label') + .column('cts') + .execute() + await db.schema + .createIndex('feed_item_originator_cursor_idx') + .on('feed_item') + .columns(['originatorDid', 'sortAt', 'cid']) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropIndex('label_cts_idx').execute() + await db.schema.dropIndex('feed_item_originator_cursor_idx').execute() +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index f723e73119f..05bbb8c6fcc 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -53,3 +53,4 @@ export * as _20230523T183902064Z from './20230523T183902064Z-algo-whats-hot-view export * as _20230529T222706121Z from './20230529T222706121Z-suggested-follows' export * as _20230530T213530067Z from './20230530T213530067Z-rebase-indices' export * as _20230605T235529700Z from './20230605T235529700Z-outgoing-repo-seq' +export * as _20230703T044601833Z from './20230703T044601833Z-feed-and-label-indices' From fde94736bc809b80eb266ddd2758b98dcf4ea23b Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 5 Jul 2023 20:40:49 -0500 Subject: [PATCH 024/237] Desanctify hellthread (#1289) * remove blessed hellthread * tweak hot classic labels --- packages/bsky/src/feed-gen/hot-classic.ts | 10 +--------- .../bsky/src/services/indexing/plugins/post.ts | 17 ++++------------- .../app-view/services/indexing/plugins/post.ts | 4 +--- packages/pds/src/feed-gen/hot-classic.ts | 10 +--------- 4 files changed, 7 insertions(+), 34 deletions(-) diff --git a/packages/bsky/src/feed-gen/hot-classic.ts b/packages/bsky/src/feed-gen/hot-classic.ts index 672aee24edb..67a9522d0c3 100644 --- a/packages/bsky/src/feed-gen/hot-classic.ts +++ b/packages/bsky/src/feed-gen/hot-classic.ts @@ -6,15 +6,7 @@ import { AlgoHandler, AlgoResponse } from './types' import { FeedKeyset } from '../api/app/bsky/util/feed' import { valuesList } from '../db/util' -const NO_WHATS_HOT_LABELS: NotEmptyArray = [ - '!no-promote', - 'corpse', - 'self-harm', - 'porn', - 'sexual', - 'nudity', - 'underwear', -] +const NO_WHATS_HOT_LABELS: NotEmptyArray = ['!no-promote'] const handler: AlgoHandler = async ( ctx: AppContext, diff --git a/packages/bsky/src/services/indexing/plugins/post.ts b/packages/bsky/src/services/indexing/plugins/post.ts index ec62ca24e17..a8e9a5cf988 100644 --- a/packages/bsky/src/services/indexing/plugins/post.ts +++ b/packages/bsky/src/services/indexing/plugins/post.ts @@ -47,8 +47,6 @@ type IndexedPost = { const lexId = lex.ids.AppBskyFeedPost const REPLY_NOTIF_DEPTH = 5 -const BLESSED_HELL_THREAD = - 'at://did:plc:wgaezxqi2spqm3mhrb5xvkzi/app.bsky.feed.post/3juzlwllznd24' const insertFn = async ( db: DatabaseSchema, @@ -153,15 +151,14 @@ const insertFn = async ( const ancestors = await getAncestorsAndSelfQb(db, { uri: post.uri, - parentHeight: - post.replyRoot === BLESSED_HELL_THREAD ? 100 : REPLY_NOTIF_DEPTH, + parentHeight: REPLY_NOTIF_DEPTH, }) .selectFrom('ancestor') .selectAll() .execute() const descendents = await getDescendentsQb(db, { uri: post.uri, - depth: post.replyRoot === BLESSED_HELL_THREAD ? 100 : REPLY_NOTIF_DEPTH, + depth: REPLY_NOTIF_DEPTH, }) .selectFrom('descendent') .innerJoin('post', 'post.uri', 'descendent.uri') @@ -221,10 +218,7 @@ const notifsForInsert = (obj: IndexedPost) => { for (const ancestor of obj.ancestors ?? []) { if (ancestor.uri === obj.post.uri) continue // no need to notify for own post - if ( - ancestor.height < REPLY_NOTIF_DEPTH || - obj.post.replyRoot === BLESSED_HELL_THREAD - ) { + if (ancestor.height < REPLY_NOTIF_DEPTH) { const ancestorUri = new AtUri(ancestor.uri) maybeNotify({ did: ancestorUri.host, @@ -243,10 +237,7 @@ const notifsForInsert = (obj: IndexedPost) => { for (const descendent of obj.descendents ?? []) { for (const ancestor of obj.ancestors ?? []) { const totalHeight = descendent.depth + ancestor.height - if ( - totalHeight < REPLY_NOTIF_DEPTH || - obj.post.replyRoot === BLESSED_HELL_THREAD - ) { + if (totalHeight < REPLY_NOTIF_DEPTH) { const ancestorUri = new AtUri(ancestor.uri) maybeNotify({ did: ancestorUri.host, diff --git a/packages/pds/src/app-view/services/indexing/plugins/post.ts b/packages/pds/src/app-view/services/indexing/plugins/post.ts index 91fa5d902f3..52db4821efb 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/post.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/post.ts @@ -208,10 +208,8 @@ const notifsForInsert = (obj: IndexedPost) => { } } const ancestors = [...obj.ancestors].sort((a, b) => a.depth - b.depth) - const BLESSED_HELL_THREAD = - 'at://did:plc:wgaezxqi2spqm3mhrb5xvkzi/app.bsky.feed.post/3juzlwllznd24' for (const relation of ancestors) { - if (relation.depth < 5 || obj.post.replyRoot === BLESSED_HELL_THREAD) { + if (relation.depth < 5) { const ancestorUri = new AtUri(relation.ancestorUri) maybeNotify({ userDid: ancestorUri.host, diff --git a/packages/pds/src/feed-gen/hot-classic.ts b/packages/pds/src/feed-gen/hot-classic.ts index bdcba7de17e..5284632a7e9 100644 --- a/packages/pds/src/feed-gen/hot-classic.ts +++ b/packages/pds/src/feed-gen/hot-classic.ts @@ -6,15 +6,7 @@ import { AlgoHandler, AlgoResponse } from './types' import { FeedKeyset } from '../app-view/api/app/bsky/util/feed' import { valuesList } from '../db/util' -const NO_WHATS_HOT_LABELS: NotEmptyArray = [ - '!no-promote', - 'corpse', - 'self-harm', - 'porn', - 'sexual', - 'nudity', - 'underwear', -] +const NO_WHATS_HOT_LABELS: NotEmptyArray = ['!no-promote'] const handler: AlgoHandler = async ( ctx: AppContext, From 7e0201d4d6d1a69fe255b6643f3d0b745d2ee9e9 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 5 Jul 2023 20:54:26 -0500 Subject: [PATCH 025/237] Bump package versions (#1288) * v0.3.0 * bump common-web to 0.2.0 * v0.2.0 * v0.2.0 * v0.1.0 * v0.1.0 * v0.1.0 * v0.2.0 * v0.4.0 * v0.1.0 * v0.2.0 * v0.2.0 * v0.3.0 * v0.2.0 --- packages/api/package.json | 2 +- packages/aws/package.json | 2 +- packages/common-web/package.json | 2 +- packages/common/package.json | 2 +- packages/crypto/package.json | 2 +- packages/identifier/package.json | 2 +- packages/identity/package.json | 2 +- packages/lex-cli/package.json | 2 +- packages/lexicon/package.json | 2 +- packages/nsid/package.json | 2 +- packages/repo/package.json | 2 +- packages/uri/package.json | 2 +- packages/xrpc-server/package.json | 2 +- packages/xrpc/package.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/api/package.json b/packages/api/package.json index 2171063c887..7353eee0a75 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.3.13", + "version": "0.4.0", "main": "src/index.ts", "scripts": { "codegen": "lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", diff --git a/packages/aws/package.json b/packages/aws/package.json index faedeb69c2f..5a8d0eb0cd7 100644 --- a/packages/aws/package.json +++ b/packages/aws/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/aws", - "version": "0.0.1", + "version": "0.1.0", "main": "src/index.ts", "license": "MIT", "repository": { diff --git a/packages/common-web/package.json b/packages/common-web/package.json index fbbe4755c7a..d5737b0f7e4 100644 --- a/packages/common-web/package.json +++ b/packages/common-web/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/common-web", - "version": "0.1.0", + "version": "0.2.0", "main": "src/index.ts", "license": "MIT", "repository": { diff --git a/packages/common/package.json b/packages/common/package.json index 580efdb2f55..11d5cbf98dd 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/common", - "version": "0.2.0", + "version": "0.3.0", "main": "src/index.ts", "license": "MIT", "repository": { diff --git a/packages/crypto/package.json b/packages/crypto/package.json index f478ab6aa45..bccaaa42c12 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/crypto", - "version": "0.1.1", + "version": "0.2.0", "main": "src/index.ts", "license": "MIT", "repository": { diff --git a/packages/identifier/package.json b/packages/identifier/package.json index cf433419e5f..1a7366c5f7e 100644 --- a/packages/identifier/package.json +++ b/packages/identifier/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/identifier", - "version": "0.1.0", + "version": "0.2.0", "main": "src/index.ts", "scripts": { "test": "jest", diff --git a/packages/identity/package.json b/packages/identity/package.json index 5474cae509e..f8e507c0672 100644 --- a/packages/identity/package.json +++ b/packages/identity/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/identity", - "version": "0.0.1", + "version": "0.1.0", "main": "src/index.ts", "license": "MIT", "repository": { diff --git a/packages/lex-cli/package.json b/packages/lex-cli/package.json index ca0725f146b..1c5a0ca5960 100644 --- a/packages/lex-cli/package.json +++ b/packages/lex-cli/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/lex-cli", - "version": "0.1.0", + "version": "0.2.0", "bin": { "lex": "dist/index.js" }, diff --git a/packages/lexicon/package.json b/packages/lexicon/package.json index 1f1a3fe5834..afca71f9d50 100644 --- a/packages/lexicon/package.json +++ b/packages/lexicon/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/lexicon", - "version": "0.1.0", + "version": "0.2.0", "main": "src/index.ts", "scripts": { "test": "jest", diff --git a/packages/nsid/package.json b/packages/nsid/package.json index 5bb2180cde5..201abfc43f0 100644 --- a/packages/nsid/package.json +++ b/packages/nsid/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/nsid", - "version": "0.0.1", + "version": "0.1.0", "main": "src/index.ts", "scripts": { "test": "jest", diff --git a/packages/repo/package.json b/packages/repo/package.json index efb689d85b2..3f6ca791a77 100644 --- a/packages/repo/package.json +++ b/packages/repo/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/repo", - "version": "0.1.0", + "version": "0.2.0", "main": "src/index.ts", "license": "MIT", "repository": { diff --git a/packages/uri/package.json b/packages/uri/package.json index 67dfec18ad8..5f3af4aa9a8 100644 --- a/packages/uri/package.json +++ b/packages/uri/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/uri", - "version": "0.0.2", + "version": "0.1.0", "main": "src/index.ts", "scripts": { "test": "jest", diff --git a/packages/xrpc-server/package.json b/packages/xrpc-server/package.json index 4ae5eed391e..f9c03afe347 100644 --- a/packages/xrpc-server/package.json +++ b/packages/xrpc-server/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/xrpc-server", - "version": "0.2.0", + "version": "0.3.0", "main": "src/index.ts", "scripts": { "test": "jest", diff --git a/packages/xrpc/package.json b/packages/xrpc/package.json index 58658647f46..63ad8955ce9 100644 --- a/packages/xrpc/package.json +++ b/packages/xrpc/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/xrpc", - "version": "0.1.0", + "version": "0.2.0", "main": "src/index.ts", "scripts": { "prettier": "prettier --check src/", From 60e8284b15fb63bd7cb46028638f6dab8b789ab3 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 6 Jul 2023 08:57:41 -0500 Subject: [PATCH 026/237] Fix flaky db test (#1287) fix flaky db test --- packages/pds/tests/db.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/pds/tests/db.test.ts b/packages/pds/tests/db.test.ts index 5ebbc788eeb..ade4de3fa3f 100644 --- a/packages/pds/tests/db.test.ts +++ b/packages/pds/tests/db.test.ts @@ -211,11 +211,11 @@ describe('db', () => { const leader2 = new Leader(777, db) const leader3 = new Leader(777, db) const result1 = await leader1.run(task) - await wait(1) // Short grace period for pg to close session + await wait(5) // Short grace period for pg to close session const result2 = await leader2.run(task) - await wait(1) + await wait(5) const result3 = await leader3.run(task) - await wait(1) + await wait(5) const result4 = await leader3.run(task) expect([result1, result2, result3, result4]).toEqual([ { ran: true, result: 'complete' }, @@ -226,7 +226,7 @@ describe('db', () => { }) it('only allows one leader at a time.', async () => { - await wait(1) + await wait(5) const task = async () => { await wait(25) return 'complete' @@ -245,7 +245,7 @@ describe('db', () => { }) it('leaders with different ids do not conflict.', async () => { - await wait(1) + await wait(5) const task = async () => { await wait(25) return 'complete' From 3ea892bc5766abad5a7ae80394ee50480d180cda Mon Sep 17 00:00:00 2001 From: devin ivy Date: Thu, 6 Jul 2023 15:18:52 -0400 Subject: [PATCH 027/237] Add administrative triage role, update moderator role (#1216) * setup triage user on pds, update moderator username (invalidates old token) * initial pass on triage access on pds, limit access to email addrs * apply moderator vs triage rules on taking and reversing mod actions for pds * update pds tests for triage auth role * setup moderator and triage roles on bsky appview * apply mod and triage access rules to bsky admin endpoints * reframe admin auth as role-based auth, tidy auth apis * tidy * build * revert change to basic auth username for role-based auth --- .github/workflows/build-and-push-pds-aws.yaml | 1 + packages/bsky/src/api/auth.ts | 65 -------- .../com/atproto/admin/getModerationAction.ts | 3 +- .../com/atproto/admin/getModerationActions.ts | 3 +- .../com/atproto/admin/getModerationReport.ts | 3 +- .../com/atproto/admin/getModerationReports.ts | 3 +- .../src/api/com/atproto/admin/getRecord.ts | 3 +- .../bsky/src/api/com/atproto/admin/getRepo.ts | 3 +- .../atproto/admin/resolveModerationReports.ts | 3 +- .../atproto/admin/reverseModerationAction.ts | 36 ++++- .../src/api/com/atproto/admin/searchRepos.ts | 3 +- .../com/atproto/admin/takeModerationAction.ts | 36 ++++- packages/bsky/src/auth.ts | 52 +++++- packages/bsky/src/config.ts | 14 ++ packages/bsky/src/context.ts | 4 + packages/bsky/tests/moderation.test.ts | 153 +++++++++++++----- packages/dev-env/src/bsky.ts | 22 +++ packages/dev-env/src/pds.ts | 19 ++- .../atproto/admin/disableAccountInvites.ts | 8 +- .../com/atproto/admin/disableInviteCodes.ts | 9 +- .../com/atproto/admin/enableAccountInvites.ts | 8 +- .../api/com/atproto/admin/getInviteCodes.ts | 2 +- .../com/atproto/admin/getModerationAction.ts | 9 +- .../com/atproto/admin/getModerationActions.ts | 2 +- .../com/atproto/admin/getModerationReport.ts | 9 +- .../com/atproto/admin/getModerationReports.ts | 2 +- .../src/api/com/atproto/admin/getRecord.ts | 9 +- .../pds/src/api/com/atproto/admin/getRepo.ts | 9 +- .../src/api/com/atproto/admin/rebaseRepo.ts | 9 +- .../atproto/admin/resolveModerationReports.ts | 2 +- .../atproto/admin/reverseModerationAction.ts | 35 +++- .../src/api/com/atproto/admin/searchRepos.ts | 13 +- .../com/atproto/admin/takeModerationAction.ts | 36 +++-- .../com/atproto/admin/updateAccountEmail.ts | 9 +- .../com/atproto/admin/updateAccountHandle.ts | 9 +- .../com/atproto/server/createInviteCode.ts | 8 +- .../com/atproto/server/createInviteCodes.ts | 10 +- packages/pds/src/auth.ts | 48 +++--- packages/pds/src/config.ts | 7 + packages/pds/src/context.ts | 8 +- packages/pds/src/index.ts | 1 + packages/pds/src/services/moderation/views.ts | 53 ++++-- packages/pds/tests/_util.ts | 6 + packages/pds/tests/account.test.ts | 15 +- packages/pds/tests/handles.test.ts | 16 +- packages/pds/tests/moderation.test.ts | 74 ++++++--- .../pds/tests/views/admin/get-repo.test.ts | 21 +++ .../pds/tests/views/admin/invites.test.ts | 8 +- 48 files changed, 602 insertions(+), 279 deletions(-) delete mode 100644 packages/bsky/src/api/auth.ts diff --git a/.github/workflows/build-and-push-pds-aws.yaml b/.github/workflows/build-and-push-pds-aws.yaml index 2565b38e432..674c9bd44cd 100644 --- a/.github/workflows/build-and-push-pds-aws.yaml +++ b/.github/workflows/build-and-push-pds-aws.yaml @@ -3,6 +3,7 @@ on: push: branches: - main + - auth-triage-role env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} diff --git a/packages/bsky/src/api/auth.ts b/packages/bsky/src/api/auth.ts deleted file mode 100644 index ec9b8112168..00000000000 --- a/packages/bsky/src/api/auth.ts +++ /dev/null @@ -1,65 +0,0 @@ -import express from 'express' -import * as uint8arrays from 'uint8arrays' -import { AuthRequiredError } from '@atproto/xrpc-server' - -const BASIC = 'Basic ' -const BEARER = 'Bearer ' - -// @TODO(bsky) treating did as a bearer, just a placeholder for now. -export const authVerifier = (ctx: { - req: express.Request - res: express.Response -}) => { - const { authorization = '' } = ctx.req.headers - if (!authorization.startsWith(BEARER)) { - throw new AuthRequiredError() - } - const did = authorization.replace(BEARER, '').trim() - if (!did.startsWith('did:')) { - throw new AuthRequiredError() - } - return { credentials: { did } } -} - -export const authOptionalVerifier = (ctx: { - req: express.Request - res: express.Response -}) => { - if (!ctx.req.headers.authorization) { - return { credentials: { did: null } } - } - return authVerifier(ctx) -} - -export const adminVerifier = - (adminPassword: string) => - (ctx: { req: express.Request; res: express.Response }) => { - const { authorization = '' } = ctx.req.headers - const parsed = parseBasicAuth(authorization) - if (!parsed) { - throw new AuthRequiredError() - } - const { username, password } = parsed - if (username !== 'admin' || password !== adminPassword) { - throw new AuthRequiredError() - } - return { credentials: { admin: true } } - } - -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 = uint8arrays - .toString(uint8arrays.fromString(b64, 'base64pad'), 'utf8') - .split(':') - } catch (err) { - return null - } - const [username, password] = parsed - if (!username || !password) return null - return { username, password } -} diff --git a/packages/bsky/src/api/com/atproto/admin/getModerationAction.ts b/packages/bsky/src/api/com/atproto/admin/getModerationAction.ts index 86fa2c94eec..ee0bf59538f 100644 --- a/packages/bsky/src/api/com/atproto/admin/getModerationAction.ts +++ b/packages/bsky/src/api/com/atproto/admin/getModerationAction.ts @@ -1,10 +1,9 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { adminVerifier } from '../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationAction({ - auth: adminVerifier(ctx.cfg.adminPassword), + auth: ctx.roleVerifier, handler: async ({ params }) => { const { db, services } = ctx const { id } = params diff --git a/packages/bsky/src/api/com/atproto/admin/getModerationActions.ts b/packages/bsky/src/api/com/atproto/admin/getModerationActions.ts index f677906ebff..2231eb686c3 100644 --- a/packages/bsky/src/api/com/atproto/admin/getModerationActions.ts +++ b/packages/bsky/src/api/com/atproto/admin/getModerationActions.ts @@ -1,10 +1,9 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { adminVerifier } from '../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationActions({ - auth: adminVerifier(ctx.cfg.adminPassword), + auth: ctx.roleVerifier, handler: async ({ params }) => { const { db, services } = ctx const { subject, limit = 50, cursor } = params diff --git a/packages/bsky/src/api/com/atproto/admin/getModerationReport.ts b/packages/bsky/src/api/com/atproto/admin/getModerationReport.ts index 25cfbb4acb9..1752a1481c1 100644 --- a/packages/bsky/src/api/com/atproto/admin/getModerationReport.ts +++ b/packages/bsky/src/api/com/atproto/admin/getModerationReport.ts @@ -1,10 +1,9 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { adminVerifier } from '../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationReport({ - auth: adminVerifier(ctx.cfg.adminPassword), + auth: ctx.roleVerifier, handler: async ({ params }) => { const { db, services } = ctx const { id } = params diff --git a/packages/bsky/src/api/com/atproto/admin/getModerationReports.ts b/packages/bsky/src/api/com/atproto/admin/getModerationReports.ts index c344539eaa6..5eb74b7e022 100644 --- a/packages/bsky/src/api/com/atproto/admin/getModerationReports.ts +++ b/packages/bsky/src/api/com/atproto/admin/getModerationReports.ts @@ -1,10 +1,9 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { adminVerifier } from '../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationReports({ - auth: adminVerifier(ctx.cfg.adminPassword), + auth: ctx.roleVerifier, handler: async ({ params }) => { const { db, services } = ctx const { diff --git a/packages/bsky/src/api/com/atproto/admin/getRecord.ts b/packages/bsky/src/api/com/atproto/admin/getRecord.ts index 3081c2cf4e4..1847ee8e7b5 100644 --- a/packages/bsky/src/api/com/atproto/admin/getRecord.ts +++ b/packages/bsky/src/api/com/atproto/admin/getRecord.ts @@ -1,11 +1,10 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { adminVerifier } from '../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getRecord({ - auth: adminVerifier(ctx.cfg.adminPassword), + auth: ctx.roleVerifier, handler: async ({ params }) => { const { db, services } = ctx const { uri, cid } = params diff --git a/packages/bsky/src/api/com/atproto/admin/getRepo.ts b/packages/bsky/src/api/com/atproto/admin/getRepo.ts index 08f7770e5f4..1e00c565ded 100644 --- a/packages/bsky/src/api/com/atproto/admin/getRepo.ts +++ b/packages/bsky/src/api/com/atproto/admin/getRepo.ts @@ -1,11 +1,10 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { adminVerifier } from '../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getRepo({ - auth: adminVerifier(ctx.cfg.adminPassword), + auth: ctx.roleVerifier, handler: async ({ params }) => { const { db, services } = ctx const { did } = params diff --git a/packages/bsky/src/api/com/atproto/admin/resolveModerationReports.ts b/packages/bsky/src/api/com/atproto/admin/resolveModerationReports.ts index 63b1e837fc9..6ec217b0655 100644 --- a/packages/bsky/src/api/com/atproto/admin/resolveModerationReports.ts +++ b/packages/bsky/src/api/com/atproto/admin/resolveModerationReports.ts @@ -1,10 +1,9 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { adminVerifier } from '../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.resolveModerationReports({ - auth: adminVerifier(ctx.cfg.adminPassword), + auth: ctx.roleVerifier, handler: async ({ input }) => { const { db, services } = ctx const moderationService = services.moderation(db) diff --git a/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts b/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts index 259e0a69c3f..0a3e1126ba8 100644 --- a/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts +++ b/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts @@ -1,14 +1,18 @@ import { AtUri } from '@atproto/uri' -import { InvalidRequestError } from '@atproto/xrpc-server' +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { + ACKNOWLEDGE, + ESCALATE, + TAKEDOWN, +} from '../../../../lexicon/types/com/atproto/admin/defs' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { TAKEDOWN } from '../../../../lexicon/types/com/atproto/admin/defs' -import { adminVerifier } from '../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.reverseModerationAction({ - auth: adminVerifier(ctx.cfg.adminPassword), - handler: async ({ input }) => { + auth: ctx.roleVerifier, + handler: async ({ input, auth }) => { + const access = auth.credentials const { db, services } = ctx const moderationService = services.moderation(db) const { id, createdBy, reason } = input.body @@ -28,6 +32,28 @@ export default function (server: Server, ctx: AppContext) { ) } + // apply access rules + + // if less than moderator access then can only reverse ack and escalation actions + if ( + !access.moderator && + ![ACKNOWLEDGE, ESCALATE].includes(existing.action) + ) { + throw new AuthRequiredError( + 'Must be a full moderator to reverse this type of action', + ) + } + // if less than admin access then can reverse takedown on an account + if ( + !access.admin && + existing.action === TAKEDOWN && + existing.subjectType === 'com.atproto.admin.defs#repoRef' + ) { + throw new AuthRequiredError( + 'Must be an admin to reverse an account takedown', + ) + } + const result = await moderationTxn.logReverseAction({ id, createdAt: now, diff --git a/packages/bsky/src/api/com/atproto/admin/searchRepos.ts b/packages/bsky/src/api/com/atproto/admin/searchRepos.ts index c81218ba93f..95e47d4078a 100644 --- a/packages/bsky/src/api/com/atproto/admin/searchRepos.ts +++ b/packages/bsky/src/api/com/atproto/admin/searchRepos.ts @@ -1,13 +1,12 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { adminVerifier } from '../../../auth' import { paginate } from '../../../../db/pagination' import { ListKeyset } from '../../../../services/actor' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.searchRepos({ - auth: adminVerifier(ctx.cfg.adminPassword), + auth: ctx.roleVerifier, handler: async ({ params }) => { const { db, services } = ctx const moderationService = services.moderation(db) diff --git a/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts b/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts index 773b7f74eb5..42e1b7034d7 100644 --- a/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts +++ b/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts @@ -1,16 +1,20 @@ import { CID } from 'multiformats/cid' import { AtUri } from '@atproto/uri' +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { TAKEDOWN } from '../../../../lexicon/types/com/atproto/admin/defs' +import { + ACKNOWLEDGE, + ESCALATE, + TAKEDOWN, +} from '../../../../lexicon/types/com/atproto/admin/defs' import { getSubject, getAction } from '../moderation/util' -import { InvalidRequestError } from '@atproto/xrpc-server' -import { adminVerifier } from '../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.takeModerationAction({ - auth: adminVerifier(ctx.cfg.adminPassword), - handler: async ({ input }) => { + auth: ctx.roleVerifier, + handler: async ({ input, auth }) => { + const access = auth.credentials const { db, services } = ctx const moderationService = services.moderation(db) const { @@ -23,6 +27,28 @@ export default function (server: Server, ctx: AppContext) { subjectBlobCids, } = input.body + // apply access rules + + // if less than admin access then can not takedown an account + if (!access.admin && action === TAKEDOWN && 'did' in subject) { + throw new AuthRequiredError( + 'Must be an admin to perform an account takedown', + ) + } + // if less than moderator access then can only take ack and escalation actions + if (!access.moderator && ![ACKNOWLEDGE, ESCALATE].includes(action)) { + 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 && + (createLabelVals?.length || negateLabelVals?.length) + ) { + throw new AuthRequiredError('Must be a full moderator to label content') + } + validateLabels([...(createLabelVals ?? []), ...(negateLabelVals ?? [])]) const moderationAction = await db.transaction(async (dbTxn) => { diff --git a/packages/bsky/src/auth.ts b/packages/bsky/src/auth.ts index 2a2a960f7f9..cedf0de6b6f 100644 --- a/packages/bsky/src/auth.ts +++ b/packages/bsky/src/auth.ts @@ -1,6 +1,11 @@ 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' + +const BASIC = 'Basic ' +const BEARER = 'Bearer ' export const authVerifier = (idResolver: IdResolver, opts: { aud: string | null }) => @@ -25,10 +30,53 @@ export const authOptionalVerifier = return authVerifier(idResolver, opts)(reqCtx) } +export const roleVerifier = + (cfg: ServerConfig) => + async (reqCtx: { req: express.Request; res: express.Response }) => { + const credentials = getRoleCredentials(cfg, reqCtx.req) + if (!credentials.valid) { + throw new AuthRequiredError() + } + return { credentials } + } + +export const getRoleCredentials = (cfg: ServerConfig, req: express.Request) => { + const parsed = parseBasicAuth(req.headers.authorization || '') + const { username, password } = parsed ?? {} + if (username === 'admin' && password === cfg.triagePassword) { + return { valid: true, admin: false, moderator: false, triage: true } + } + if (username === 'admin' && password === cfg.moderatorPassword) { + return { valid: true, admin: false, moderator: true, triage: true } + } + if (username === 'admin' && password === cfg.adminPassword) { + return { valid: true, admin: true, moderator: true, triage: true } + } + return { valid: false, admin: false, moderator: false, triage: false } +} + +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 = uint8arrays + .toString(uint8arrays.fromString(b64, 'base64pad'), 'utf8') + .split(':') + } catch (err) { + return null + } + const [username, password] = parsed + if (!username || !password) return null + return { username, password } +} + export const getJwtStrFromReq = (req: express.Request): string | null => { const { authorization = '' } = req.headers - if (!authorization.startsWith('Bearer ')) { + if (!authorization.startsWith(BEARER)) { return null } - return authorization.replace('Bearer ', '').trim() + return authorization.replace(BEARER, '').trim() } diff --git a/packages/bsky/src/config.ts b/packages/bsky/src/config.ts index 6d14771731b..83081a2a1ad 100644 --- a/packages/bsky/src/config.ts +++ b/packages/bsky/src/config.ts @@ -22,6 +22,8 @@ export interface ServerConfigValues { labelerDid: string hiveApiKey?: string adminPassword: string + moderatorPassword?: string + triagePassword?: string labelerKeywords: Record indexerConcurrency?: number } @@ -60,6 +62,8 @@ export class ServerConfig { const dbPostgresSchema = process.env.DB_POSTGRES_SCHEMA const repoProvider = process.env.REPO_PROVIDER // E.g. ws://abc.com:4000 const adminPassword = process.env.ADMIN_PASSWORD || 'admin' + const moderatorPassword = process.env.MODERATOR_PASSWORD || undefined + const triagePassword = process.env.TRIAGE_PASSWORD || undefined const labelerDid = process.env.LABELER_DID || 'did:example:labeler' const hiveApiKey = process.env.HIVE_API_KEY || undefined const indexerConcurrency = maybeParseInt(process.env.INDEXER_CONCURRENCY) @@ -84,6 +88,8 @@ export class ServerConfig { labelerDid, hiveApiKey, adminPassword, + moderatorPassword, + triagePassword, labelerKeywords, indexerConcurrency, ...stripUndefineds(overrides ?? {}), @@ -187,6 +193,14 @@ export class ServerConfig { return this.cfg.adminPassword } + get moderatorPassword() { + return this.cfg.moderatorPassword + } + + get triagePassword() { + return this.cfg.triagePassword + } + get indexerConcurrency() { return this.cfg.indexerConcurrency } diff --git a/packages/bsky/src/context.ts b/packages/bsky/src/context.ts index aee4706eee6..a3761321b62 100644 --- a/packages/bsky/src/context.ts +++ b/packages/bsky/src/context.ts @@ -67,6 +67,10 @@ export class AppContext { }) } + get roleVerifier() { + return auth.roleVerifier(this.cfg) + } + get labeler(): Labeler { return this.opts.labeler } diff --git a/packages/bsky/tests/moderation.test.ts b/packages/bsky/tests/moderation.test.ts index c490f1a36e0..08067ef61f7 100644 --- a/packages/bsky/tests/moderation.test.ts +++ b/packages/bsky/tests/moderation.test.ts @@ -1,4 +1,5 @@ import { TestNetwork } from '@atproto/dev-env' +import { TID, cidForCbor } from '@atproto/common' import AtpAgent, { ComAtprotoAdminTakeModerationAction } from '@atproto/api' import { AtUri } from '@atproto/uri' import { forSnapshot } from './_util' @@ -6,6 +7,7 @@ import { ImageRef, RecordRef, SeedClient } from './seeds/client' import basicSeed from './seeds/basic' import { ACKNOWLEDGE, + ESCALATE, FLAG, TAKEDOWN, } from '../src/lexicon/types/com/atproto/admin/defs' @@ -13,7 +15,6 @@ import { REASONOTHER, REASONSPAM, } from '../src/lexicon/types/com/atproto/moderation/defs' -import { TID, cidForCbor } from '@atproto/common' describe('moderation', () => { let network: TestNetwork @@ -210,7 +211,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) const { data: actionResolvedReports } = @@ -222,7 +223,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -237,7 +238,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) @@ -292,7 +293,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) await agent.api.com.atproto.admin.resolveModerationReports( @@ -303,24 +304,24 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) // Check report and action details const { data: recordActionDetail } = await agent.api.com.atproto.admin.getModerationAction( { id: action.id }, - { headers: network.pds.adminAuthHeaders() }, + { headers: network.bsky.adminAuthHeaders() }, ) const { data: reportADetail } = await agent.api.com.atproto.admin.getModerationReport( { id: reportA.id }, - { headers: network.pds.adminAuthHeaders() }, + { headers: network.bsky.adminAuthHeaders() }, ) const { data: reportBDetail } = await agent.api.com.atproto.admin.getModerationReport( { id: reportB.id }, - { headers: network.pds.adminAuthHeaders() }, + { headers: network.bsky.adminAuthHeaders() }, ) expect( forSnapshot({ @@ -338,7 +339,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) @@ -371,7 +372,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -383,7 +384,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -400,7 +401,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) @@ -437,7 +438,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -449,7 +450,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -466,18 +467,18 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) - it('supports flagging and acknowledging.', async () => { + it('supports escalating and acknowledging for triage.', async () => { const postRef1 = sc.posts[sc.dids.alice][0].ref const postRef2 = sc.posts[sc.dids.bob][0].ref const { data: action1 } = await agent.api.com.atproto.admin.takeModerationAction( { - action: FLAG, + action: ESCALATE, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef1.uri.toString(), @@ -488,12 +489,12 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders('triage'), }, ) expect(action1).toEqual( expect.objectContaining({ - action: FLAG, + action: ESCALATE, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef1.uriStr, @@ -515,7 +516,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders('triage'), }, ) expect(action2).toEqual( @@ -537,7 +538,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders('triage'), }, ) await agent.api.com.atproto.admin.reverseModerationAction( @@ -548,7 +549,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders('triage'), }, ) }) @@ -569,7 +570,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) const flagPromise = agent.api.com.atproto.admin.takeModerationAction( @@ -585,7 +586,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) await expect(flagPromise).rejects.toThrow( @@ -601,7 +602,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) const { data: flag } = @@ -618,7 +619,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -631,7 +632,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) @@ -650,7 +651,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) const flagPromise = agent.api.com.atproto.admin.takeModerationAction( @@ -665,7 +666,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) await expect(flagPromise).rejects.toThrow( @@ -681,7 +682,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) const { data: flag } = @@ -697,7 +698,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -710,7 +711,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) @@ -734,7 +735,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) const flagPromise = agent.api.com.atproto.admin.takeModerationAction( @@ -751,7 +752,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) await expect(flagPromise).rejects.toThrow( @@ -766,7 +767,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) const { data: flag } = @@ -784,7 +785,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) @@ -797,7 +798,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) }) @@ -937,6 +938,70 @@ describe('moderation', () => { await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual(['kittens']) }) + it('does not allow triage moderators to label.', async () => { + const attemptLabel = agent.api.com.atproto.admin.takeModerationAction( + { + action: ACKNOWLEDGE, + createdBy: 'did:example:moderator', + reason: 'Y', + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, + negateLabelVals: ['a'], + createLabelVals: ['b', 'c'], + }, + { + encoding: 'application/json', + headers: network.bsky.adminAuthHeaders('triage'), + }, + ) + await expect(attemptLabel).rejects.toThrow( + 'Must be a full moderator to label content', + ) + }) + + it('does not allow non-admin moderators to takedown.', async () => { + const attemptTakedownMod = + agent.api.com.atproto.admin.takeModerationAction( + { + action: TAKEDOWN, + createdBy: 'did:example:moderator', + reason: 'Y', + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, + }, + { + encoding: 'application/json', + headers: network.bsky.adminAuthHeaders('moderator'), + }, + ) + await expect(attemptTakedownMod).rejects.toThrow( + 'Must be an admin to perform an account takedown', + ) + const attemptTakedownTriage = + agent.api.com.atproto.admin.takeModerationAction( + { + action: TAKEDOWN, + 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(attemptTakedownTriage).rejects.toThrow( + 'Must be an admin to perform an account takedown', + ) + }) + async function actionWithLabels( opts: Partial & { subject: ComAtprotoAdminTakeModerationAction.InputSchema['subject'] @@ -951,7 +1016,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) return result.data @@ -966,7 +1031,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) } @@ -974,7 +1039,7 @@ describe('moderation', () => { async function getRecordLabels(uri: string) { const result = await agent.api.com.atproto.admin.getRecord( { uri }, - { headers: network.pds.adminAuthHeaders() }, + { headers: network.bsky.adminAuthHeaders() }, ) const labels = result.data.labels ?? [] return labels.map((l) => l.val) @@ -983,7 +1048,7 @@ describe('moderation', () => { async function getRepoLabels(did: string) { const result = await agent.api.com.atproto.admin.getRepo( { did }, - { headers: network.pds.adminAuthHeaders() }, + { headers: network.bsky.adminAuthHeaders() }, ) const labels = result.data.labels ?? [] return labels.map((l) => l.val) @@ -1024,7 +1089,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) actionId = takeAction.data.id @@ -1055,7 +1120,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + headers: network.bsky.adminAuthHeaders(), }, ) diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index da3010adf22..03ad48fc796 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -1,4 +1,5 @@ import getPort from 'get-port' +import * as ui8 from 'uint8arrays' import * as bsky from '@atproto/bsky' import { DAY, HOUR } from '@atproto/common-web' import { AtpAgent } from '@atproto/api' @@ -43,6 +44,8 @@ export class TestBsky { // Each test suite gets its own lock id for the repo subscription repoSubLockId: uniqueLockId(), adminPassword: 'admin-pass', + moderatorPassword: 'moderator-pass', + triagePassword: 'triage-pass', labelerDid: 'did:example:labeler', labelerKeywords: { label_me: 'test-label', label_me_2: 'test-label-2' }, feedGenDid: 'did:example:feedGen', @@ -86,6 +89,25 @@ export class TestBsky { return new AtpAgent({ service: this.url }) } + adminAuth(role: 'admin' | 'moderator' | 'triage' = 'admin'): string { + const password = + role === 'triage' + ? this.ctx.cfg.triagePassword + : role === 'moderator' + ? this.ctx.cfg.moderatorPassword + : this.ctx.cfg.adminPassword + 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() } diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 28b6727c4d8..7c665304d58 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -42,6 +42,8 @@ export class TestPds { serverDid, recoveryKey: recoveryKey.did(), adminPassword: 'admin-pass', + moderatorPassword: 'moderator-pass', + triagePassword: 'triage-pass', inviteRequired: false, userInviteInterval: null, userInviteEpoch: 0, @@ -110,19 +112,22 @@ export class TestPds { return new AtpAgent({ service: `http://localhost:${this.port}` }) } - adminAuth(): string { + adminAuth(role: 'admin' | 'moderator' | 'triage' = 'admin'): string { + const password = + role === 'triage' + ? this.ctx.cfg.triagePassword + : role === 'moderator' + ? this.ctx.cfg.moderatorPassword + : this.ctx.cfg.adminPassword return ( 'Basic ' + - ui8.toString( - ui8.fromString(`admin:${this.ctx.cfg.adminPassword}`, 'utf8'), - 'base64pad', - ) + ui8.toString(ui8.fromString(`admin:${password}`, 'utf8'), 'base64pad') ) } - adminAuthHeaders() { + adminAuthHeaders(role?: 'admin' | 'moderator' | 'triage') { return { - authorization: this.adminAuth(), + authorization: this.adminAuth(role), } } diff --git a/packages/pds/src/api/com/atproto/admin/disableAccountInvites.ts b/packages/pds/src/api/com/atproto/admin/disableAccountInvites.ts index b87b2132eb7..4e6b0da2d14 100644 --- a/packages/pds/src/api/com/atproto/admin/disableAccountInvites.ts +++ b/packages/pds/src/api/com/atproto/admin/disableAccountInvites.ts @@ -1,10 +1,14 @@ +import { AuthRequiredError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.disableAccountInvites({ - auth: ctx.adminVerifier, - handler: async ({ input }) => { + auth: ctx.roleVerifier, + handler: async ({ input, auth }) => { + if (!auth.credentials.admin) { + throw new AuthRequiredError('Insufficient privileges') + } const { account } = input.body await ctx.db.db .updateTable('user_account') diff --git a/packages/pds/src/api/com/atproto/admin/disableInviteCodes.ts b/packages/pds/src/api/com/atproto/admin/disableInviteCodes.ts index fb7e387bbaf..d1fd4658b43 100644 --- a/packages/pds/src/api/com/atproto/admin/disableInviteCodes.ts +++ b/packages/pds/src/api/com/atproto/admin/disableInviteCodes.ts @@ -1,11 +1,14 @@ +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { InvalidRequestError } from '@atproto/xrpc-server' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.disableInviteCodes({ - auth: ctx.adminVerifier, - handler: async ({ input }) => { + auth: ctx.roleVerifier, + handler: async ({ input, auth }) => { + if (!auth.credentials.admin) { + throw new AuthRequiredError('Insufficient privileges') + } const { codes = [], accounts = [] } = input.body if (accounts.includes('admin')) { throw new InvalidRequestError('cannot disable admin invite codes') diff --git a/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts b/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts index f09d8f3267f..8f2648c9614 100644 --- a/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts +++ b/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts @@ -1,10 +1,14 @@ +import { AuthRequiredError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.enableAccountInvites({ - auth: ctx.adminVerifier, - handler: async ({ input }) => { + auth: ctx.roleVerifier, + handler: async ({ input, auth }) => { + if (!auth.credentials.admin) { + throw new AuthRequiredError('Insufficient privileges') + } const { account } = input.body await ctx.db.db .updateTable('user_account') diff --git a/packages/pds/src/api/com/atproto/admin/getInviteCodes.ts b/packages/pds/src/api/com/atproto/admin/getInviteCodes.ts index c5ba289032b..02e9c0af82f 100644 --- a/packages/pds/src/api/com/atproto/admin/getInviteCodes.ts +++ b/packages/pds/src/api/com/atproto/admin/getInviteCodes.ts @@ -10,7 +10,7 @@ import { export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getInviteCodes({ - auth: ctx.moderatorVerifier, + auth: ctx.roleVerifier, handler: async ({ params }) => { const { sort, limit, cursor } = params const ref = ctx.db.db.dynamic.ref diff --git a/packages/pds/src/api/com/atproto/admin/getModerationAction.ts b/packages/pds/src/api/com/atproto/admin/getModerationAction.ts index 8cecab167db..5e6ee8113c2 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationAction.ts @@ -3,15 +3,18 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationAction({ - auth: ctx.moderatorVerifier, - handler: async ({ params }) => { + auth: ctx.roleVerifier, + handler: async ({ params, auth }) => { + const access = auth.credentials const { db, services } = ctx const { id } = params const moderationService = services.moderation(db) const result = await moderationService.getActionOrThrow(id) return { encoding: 'application/json', - body: await moderationService.views.actionDetail(result), + body: await moderationService.views.actionDetail(result, { + includeEmails: access.moderator, + }), } }, }) diff --git a/packages/pds/src/api/com/atproto/admin/getModerationActions.ts b/packages/pds/src/api/com/atproto/admin/getModerationActions.ts index dfbf5e12efe..2231eb686c3 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationActions.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationActions.ts @@ -3,7 +3,7 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationActions({ - auth: ctx.moderatorVerifier, + auth: ctx.roleVerifier, handler: async ({ params }) => { const { db, services } = ctx const { subject, limit = 50, cursor } = params diff --git a/packages/pds/src/api/com/atproto/admin/getModerationReport.ts b/packages/pds/src/api/com/atproto/admin/getModerationReport.ts index 18c0c764426..66d47f01fa7 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationReport.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationReport.ts @@ -3,15 +3,18 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationReport({ - auth: ctx.moderatorVerifier, - handler: async ({ params }) => { + auth: ctx.roleVerifier, + handler: async ({ params, auth }) => { + const access = auth.credentials const { db, services } = ctx const { id } = params const moderationService = services.moderation(db) const result = await moderationService.getReportOrThrow(id) return { encoding: 'application/json', - body: await moderationService.views.reportDetail(result), + body: await moderationService.views.reportDetail(result, { + includeEmails: access.moderator, + }), } }, }) diff --git a/packages/pds/src/api/com/atproto/admin/getModerationReports.ts b/packages/pds/src/api/com/atproto/admin/getModerationReports.ts index 3eb1a26e9dd..171777088c6 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationReports.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationReports.ts @@ -3,7 +3,7 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationReports({ - auth: ctx.moderatorVerifier, + auth: ctx.roleVerifier, handler: async ({ params }) => { const { db, services } = ctx const { diff --git a/packages/pds/src/api/com/atproto/admin/getRecord.ts b/packages/pds/src/api/com/atproto/admin/getRecord.ts index 3d4f69fd6cc..53164c5c6e0 100644 --- a/packages/pds/src/api/com/atproto/admin/getRecord.ts +++ b/packages/pds/src/api/com/atproto/admin/getRecord.ts @@ -5,8 +5,9 @@ import { AtUri } from '@atproto/uri' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getRecord({ - auth: ctx.moderatorVerifier, - handler: async ({ params }) => { + auth: ctx.roleVerifier, + handler: async ({ params, auth }) => { + const access = auth.credentials const { db, services } = ctx const { uri, cid } = params const result = await services @@ -17,7 +18,9 @@ export default function (server: Server, ctx: AppContext) { } return { encoding: 'application/json', - body: await services.moderation(db).views.recordDetail(result), + body: await services.moderation(db).views.recordDetail(result, { + includeEmails: access.moderator, + }), } }, }) diff --git a/packages/pds/src/api/com/atproto/admin/getRepo.ts b/packages/pds/src/api/com/atproto/admin/getRepo.ts index 5e304409f2f..476c762bb5e 100644 --- a/packages/pds/src/api/com/atproto/admin/getRepo.ts +++ b/packages/pds/src/api/com/atproto/admin/getRepo.ts @@ -4,8 +4,9 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getRepo({ - auth: ctx.moderatorVerifier, - handler: async ({ params }) => { + auth: ctx.roleVerifier, + handler: async ({ params, auth }) => { + const access = auth.credentials const { db, services } = ctx const { did } = params const result = await services.account(db).getAccount(did, true) @@ -14,7 +15,9 @@ export default function (server: Server, ctx: AppContext) { } return { encoding: 'application/json', - body: await services.moderation(db).views.repoDetail(result), + body: await services.moderation(db).views.repoDetail(result, { + includeEmails: access.moderator, + }), } }, }) diff --git a/packages/pds/src/api/com/atproto/admin/rebaseRepo.ts b/packages/pds/src/api/com/atproto/admin/rebaseRepo.ts index 8b284da2519..bbdf61bc7a9 100644 --- a/packages/pds/src/api/com/atproto/admin/rebaseRepo.ts +++ b/packages/pds/src/api/com/atproto/admin/rebaseRepo.ts @@ -1,4 +1,4 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { CID } from 'multiformats/cid' @@ -7,8 +7,11 @@ import { ConcurrentWriteError } from '../../../../services/repo' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.rebaseRepo({ - auth: ctx.adminVerifier, - handler: async ({ input }) => { + auth: ctx.roleVerifier, + handler: async ({ input, auth }) => { + if (!auth.credentials.admin) { + throw new AuthRequiredError('Insufficient privileges') + } const { repo, swapCommit } = input.body const swapCommitCid = swapCommit ? CID.parse(swapCommit) : undefined try { diff --git a/packages/pds/src/api/com/atproto/admin/resolveModerationReports.ts b/packages/pds/src/api/com/atproto/admin/resolveModerationReports.ts index 267238e2faf..6ec217b0655 100644 --- a/packages/pds/src/api/com/atproto/admin/resolveModerationReports.ts +++ b/packages/pds/src/api/com/atproto/admin/resolveModerationReports.ts @@ -3,7 +3,7 @@ import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.resolveModerationReports({ - auth: ctx.moderatorVerifier, + auth: ctx.roleVerifier, handler: async ({ input }) => { const { db, services } = ctx const moderationService = services.moderation(db) diff --git a/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts b/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts index 0cfc2fd1f20..2d7bf94673e 100644 --- a/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts @@ -1,13 +1,18 @@ import { AtUri } from '@atproto/uri' -import { InvalidRequestError } from '@atproto/xrpc-server' +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { TAKEDOWN } from '../../../../lexicon/types/com/atproto/admin/defs' +import { + ACKNOWLEDGE, + ESCALATE, + TAKEDOWN, +} from '../../../../lexicon/types/com/atproto/admin/defs' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.reverseModerationAction({ - auth: ctx.adminVerifier, - handler: async ({ input }) => { + auth: ctx.roleVerifier, + handler: async ({ input, auth }) => { + const access = auth.credentials const { db, services } = ctx const moderationService = services.moderation(db) const { id, createdBy, reason } = input.body @@ -27,6 +32,28 @@ export default function (server: Server, ctx: AppContext) { ) } + // apply access rules + + // if less than moderator access then can only reverse ack and escalation actions + if ( + !access.moderator && + ![ACKNOWLEDGE, ESCALATE].includes(existing.action) + ) { + throw new AuthRequiredError( + 'Must be a full moderator to reverse this type of action', + ) + } + // if less than admin access then can reverse takedown on an account + if ( + !access.admin && + existing.action === TAKEDOWN && + existing.subjectType === 'com.atproto.admin.defs#repoRef' + ) { + throw new AuthRequiredError( + 'Must be an admin to reverse an account takedown', + ) + } + const result = await moderationTxn.logReverseAction({ id, createdAt: now, diff --git a/packages/pds/src/api/com/atproto/admin/searchRepos.ts b/packages/pds/src/api/com/atproto/admin/searchRepos.ts index 28506c5af4f..28a49b947c3 100644 --- a/packages/pds/src/api/com/atproto/admin/searchRepos.ts +++ b/packages/pds/src/api/com/atproto/admin/searchRepos.ts @@ -6,8 +6,9 @@ import { ListKeyset } from '../../../../services/account' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.searchRepos({ - auth: ctx.moderatorVerifier, - handler: async ({ params }) => { + auth: ctx.roleVerifier, + handler: async ({ params, auth }) => { + const access = auth.credentials const { db, services } = ctx const moderationService = services.moderation(db) const { term = '', limit = 50, cursor, invitedBy } = params @@ -22,7 +23,9 @@ export default function (server: Server, ctx: AppContext) { encoding: 'application/json', body: { cursor: keyset.packFromResult(results), - repos: await moderationService.views.repo(results), + repos: await moderationService.views.repo(results, { + includeEmails: access.moderator, + }), }, } } @@ -40,7 +43,9 @@ export default function (server: Server, ctx: AppContext) { // For did search, we can only find 1 or no match, cursors can be ignored entirely cursor: searchField === 'did' ? undefined : keyset.packFromResult(results), - repos: await moderationService.views.repo(results), + repos: await moderationService.views.repo(results, { + includeEmails: access.moderator, + }), }, } }, diff --git a/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts b/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts index 7a0931f6871..d4d0639c0f4 100644 --- a/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts @@ -1,15 +1,20 @@ import { CID } from 'multiformats/cid' import { AtUri } from '@atproto/uri' +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { TAKEDOWN } from '../../../../lexicon/types/com/atproto/admin/defs' +import { + ACKNOWLEDGE, + ESCALATE, + TAKEDOWN, +} from '../../../../lexicon/types/com/atproto/admin/defs' import { getSubject, getAction } from '../moderation/util' -import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.takeModerationAction({ - auth: ctx.moderatorVerifier, + auth: ctx.roleVerifier, handler: async ({ input, auth }) => { + const access = auth.credentials const { db, services } = ctx const moderationService = services.moderation(db) const { @@ -22,16 +27,27 @@ export default function (server: Server, ctx: AppContext) { subjectBlobCids, } = input.body - if ( - !auth.credentials.admin && - (createLabelVals?.length || - negateLabelVals?.length || - action === TAKEDOWN) - ) { + // apply access rules + + // if less than admin access then can not takedown an account + if (!access.admin && action === TAKEDOWN && 'did' in subject) { throw new AuthRequiredError( - 'Must be an admin to takedown or label content', + 'Must be an admin to perform an account takedown', ) } + // if less than moderator access then can only take ack and escalation actions + if (!access.moderator && ![ACKNOWLEDGE, ESCALATE].includes(action)) { + 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 && + (createLabelVals?.length || negateLabelVals?.length) + ) { + throw new AuthRequiredError('Must be a full moderator to label content') + } validateLabels([...(createLabelVals ?? []), ...(negateLabelVals ?? [])]) diff --git a/packages/pds/src/api/com/atproto/admin/updateAccountEmail.ts b/packages/pds/src/api/com/atproto/admin/updateAccountEmail.ts index ad759473a20..851613a6bad 100644 --- a/packages/pds/src/api/com/atproto/admin/updateAccountEmail.ts +++ b/packages/pds/src/api/com/atproto/admin/updateAccountEmail.ts @@ -1,11 +1,14 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateAccountEmail({ - auth: ctx.adminVerifier, - handler: async ({ input }) => { + auth: ctx.roleVerifier, + handler: async ({ input, auth }) => { + if (!auth.credentials.admin) { + throw new AuthRequiredError('Insufficient privileges') + } await ctx.db.transaction(async (dbTxn) => { const accntService = ctx.services.account(dbTxn) const account = await accntService.getAccount(input.body.account) diff --git a/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts b/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts index 6d307701f2e..36118cf77a7 100644 --- a/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts +++ b/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts @@ -1,4 +1,4 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import * as ident from '@atproto/identifier' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' @@ -6,8 +6,11 @@ import { UserAlreadyExistsError } from '../../../../services/account' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateAccountHandle({ - auth: ctx.adminVerifier, - handler: async ({ input, req }) => { + auth: ctx.roleVerifier, + handler: async ({ input, req, auth }) => { + if (!auth.credentials.admin) { + throw new AuthRequiredError('Insufficient privileges') + } const { did } = input.body let handle: string try { diff --git a/packages/pds/src/api/com/atproto/server/createInviteCode.ts b/packages/pds/src/api/com/atproto/server/createInviteCode.ts index 84a685e4b72..6c8e8b8b714 100644 --- a/packages/pds/src/api/com/atproto/server/createInviteCode.ts +++ b/packages/pds/src/api/com/atproto/server/createInviteCode.ts @@ -1,11 +1,15 @@ +import { AuthRequiredError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { genInvCode } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.createInviteCode({ - auth: ctx.adminVerifier, - handler: async ({ input, req }) => { + auth: ctx.roleVerifier, + handler: async ({ input, req, auth }) => { + if (!auth.credentials.admin) { + throw new AuthRequiredError('Insufficient privileges') + } const { useCount, forAccount = 'admin' } = input.body const code = genInvCode(ctx.cfg) diff --git a/packages/pds/src/api/com/atproto/server/createInviteCodes.ts b/packages/pds/src/api/com/atproto/server/createInviteCodes.ts index dddbf7c0893..d2d043e6ec7 100644 --- a/packages/pds/src/api/com/atproto/server/createInviteCodes.ts +++ b/packages/pds/src/api/com/atproto/server/createInviteCodes.ts @@ -1,14 +1,18 @@ +import { chunkArray } from '@atproto/common' +import { AuthRequiredError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { genInvCodes } from './util' import { InviteCode } from '../../../../db/tables/invite-code' import { AccountCodes } from '../../../../lexicon/types/com/atproto/server/createInviteCodes' -import { chunkArray } from '@atproto/common' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.createInviteCodes({ - auth: ctx.adminVerifier, - handler: async ({ input, req }) => { + auth: ctx.roleVerifier, + handler: async ({ input, req, auth }) => { + if (!auth.credentials.admin) { + throw new AuthRequiredError('Insufficient privileges') + } const { codeCount, useCount } = input.body const forAccounts = input.body.forAccounts ?? ['admin'] diff --git a/packages/pds/src/auth.ts b/packages/pds/src/auth.ts index a877ae9b343..e5cb9185143 100644 --- a/packages/pds/src/auth.ts +++ b/packages/pds/src/auth.ts @@ -13,6 +13,7 @@ export type ServerAuthOpts = { jwtSecret: string adminPass: string moderatorPass?: string + triagePass?: string } // @TODO sync-up with current method names, consider backwards compat. @@ -34,11 +35,13 @@ export class ServerAuth { private _secret: string private _adminPass: string private _moderatorPass?: string + private _triagePass?: string constructor(opts: ServerAuthOpts) { this._secret = opts.jwtSecret this._adminPass = opts.adminPass this._moderatorPass = opts.moderatorPass + this._triagePass = opts.triagePass } createAccessToken(opts: { @@ -110,18 +113,23 @@ export class ServerAuth { return authorized !== null && authorized.did === did } - verifyAdmin(req: express.Request) { + verifyRole(req: express.Request) { const parsed = parseBasicAuth(req.headers.authorization || '') + const { Missing, Valid, Invalid } = AuthStatus if (!parsed) { - return { admin: false, moderator: false, missing: true } + return { status: Missing, admin: false, moderator: false, triage: false } } const { username, password } = parsed - if (username !== 'admin') { - return { admin: false, moderator: false } + if (username === 'admin' && password === this._triagePass) { + return { status: Valid, admin: false, moderator: false, triage: true } } - const admin = password === this._adminPass - const moderator = admin || password === this._moderatorPass - return { admin, moderator } + if (username === 'admin' && password === this._moderatorPass) { + return { status: Valid, admin: false, moderator: true, triage: true } + } + if (username === 'admin' && password === this._adminPass) { + return { status: Valid, admin: true, moderator: true, triage: true } + } + return { status: Invalid, admin: false, moderator: false, triage: false } } getToken(req: express.Request) { @@ -230,8 +238,8 @@ export const optionalAccessOrAdminVerifier = (auth: ServerAuth) => { err.customErrorName === 'AuthMissing' ) { // Missing access bearer, move onto admin basic auth - const credentials = auth.verifyAdmin(ctx.req) - if (credentials.missing) { + const credentials = auth.verifyRole(ctx.req) + if (credentials.status === AuthStatus.Missing) { // If both are missing, passthrough: this auth scheme is optional return { credentials: null } } else if (credentials.admin) { @@ -270,21 +278,11 @@ export const refreshVerifier = } } -export const adminVerifier = +export const roleVerifier = (auth: ServerAuth) => async (ctx: { req: express.Request; res: express.Response }) => { - const credentials = auth.verifyAdmin(ctx.req) - if (!credentials.admin) { - throw new AuthRequiredError() - } - return { credentials } - } - -export const moderatorVerifier = - (auth: ServerAuth) => - async (ctx: { req: express.Request; res: express.Response }) => { - const credentials = auth.verifyAdmin(ctx.req) - if (!credentials.moderator) { + const credentials = auth.verifyRole(ctx.req) + if (credentials.status !== AuthStatus.Valid) { throw new AuthRequiredError() } return { credentials } @@ -293,3 +291,9 @@ export const moderatorVerifier = export const getRefreshTokenId = () => { return ui8.toString(crypto.randomBytes(32), 'base64') } + +export enum AuthStatus { + Valid, + Invalid, + Missing, +} diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index 282f7929a10..0807ae390bc 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -25,6 +25,7 @@ export interface ServerConfigValues { recoveryKey: string adminPassword: string moderatorPassword?: string + triagePassword?: string inviteRequired: boolean userInviteInterval: number | null @@ -114,6 +115,7 @@ export class ServerConfig { const adminPassword = process.env.ADMIN_PASSWORD || 'admin' const moderatorPassword = process.env.MODERATOR_PASSWORD || undefined + const triagePassword = process.env.TRIAGE_PASSWORD || undefined const inviteRequired = process.env.INVITE_REQUIRED === 'true' ? true : false const userInviteInterval = parseIntWithFallback( @@ -213,6 +215,7 @@ export class ServerConfig { serverDid, adminPassword, moderatorPassword, + triagePassword, inviteRequired, userInviteInterval, userInviteEpoch, @@ -330,6 +333,10 @@ export class ServerConfig { return this.cfg.moderatorPassword } + get triagePassword() { + return this.cfg.triagePassword + } + get inviteRequired() { return this.cfg.inviteRequired } diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 093edb1ba84..9d07e119c98 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -89,12 +89,8 @@ export class AppContext { return auth.refreshVerifier(this.auth) } - get adminVerifier() { - return auth.adminVerifier(this.auth) - } - - get moderatorVerifier() { - return auth.moderatorVerifier(this.auth) + get roleVerifier() { + return auth.roleVerifier(this.auth) } get optionalAccessOrAdminVerifier() { diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 3449425f63b..28ee3b9773d 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -90,6 +90,7 @@ export class PDS { jwtSecret: config.jwtSecret, adminPass: config.adminPassword, moderatorPass: config.moderatorPassword, + triagePass: config.triagePassword, }) const didCache = new DidSqlCache( diff --git a/packages/pds/src/services/moderation/views.ts b/packages/pds/src/services/moderation/views.ts index 35b597f70d9..51f434e83de 100644 --- a/packages/pds/src/services/moderation/views.ts +++ b/packages/pds/src/services/moderation/views.ts @@ -31,10 +31,11 @@ export class ModerationViews { record: RecordService.creator(this.messageDispatcher), } - repo(result: RepoResult): Promise - repo(result: RepoResult[]): Promise + repo(result: RepoResult, opts: ModViewOptions): Promise + repo(result: RepoResult[], opts: ModViewOptions): Promise async repo( result: RepoResult | RepoResult[], + opts: ModViewOptions, ): Promise { const results = Array.isArray(result) ? result : [result] if (results.length === 0) return [] @@ -96,7 +97,7 @@ export class ModerationViews { return { did: r.did, handle: r.handle, - email: email ?? undefined, + email: opts.includeEmails && email ? email : undefined, relatedRecords, indexedAt: r.indexedAt, moderation: { @@ -112,8 +113,11 @@ export class ModerationViews { return Array.isArray(result) ? views : views[0] } - async repoDetail(result: RepoResult): Promise { - const repo = await this.repo(result) + async repoDetail( + result: RepoResult, + opts: ModViewOptions, + ): Promise { + const repo = await this.repo(result, opts) const [reportResults, actionResults, inviteCodes] = await Promise.all([ this.db.db .selectFrom('moderation_report') @@ -148,10 +152,11 @@ export class ModerationViews { } } - record(result: RecordResult): Promise - record(result: RecordResult[]): Promise + record(result: RecordResult, opts: ModViewOptions): Promise + record(result: RecordResult[], opts: ModViewOptions): Promise async record( result: RecordResult | RecordResult[], + opts: ModViewOptions, ): Promise { const results = Array.isArray(result) ? result : [result] if (results.length === 0) return [] @@ -189,7 +194,7 @@ export class ModerationViews { .select(['id', 'action', 'subjectUri']) .execute(), ]) - const repos = await this.repo(repoResults) + const repos = await this.repo(repoResults, opts) const reposByDid = repos.reduce( (acc, cur) => Object.assign(acc, { [cur.did]: cur }), @@ -227,9 +232,12 @@ export class ModerationViews { return Array.isArray(result) ? views : views[0] } - async recordDetail(result: RecordResult): Promise { + async recordDetail( + result: RecordResult, + opts: ModViewOptions, + ): Promise { const [record, reportResults, actionResults] = await Promise.all([ - this.record(result), + this.record(result, opts), this.db.db .selectFrom('moderation_report') .where('subjectType', '=', 'com.atproto.repo.strongRef') @@ -351,7 +359,10 @@ export class ModerationViews { return Array.isArray(result) ? views : views[0] } - async actionDetail(result: ActionResult): Promise { + async actionDetail( + result: ActionResult, + opts: ModViewOptions, + ): Promise { const action = await this.action(result) const reportResults = action.resolvedReportIds.length ? await this.db.db @@ -362,7 +373,7 @@ export class ModerationViews { .execute() : [] const [subject, resolvedReports, subjectBlobs] = await Promise.all([ - this.subject(result), + this.subject(result, opts), this.report(reportResults), this.blob(action.subjectBlobCids), ]) @@ -458,7 +469,10 @@ export class ModerationViews { } } - async reportDetail(result: ReportResult): Promise { + async reportDetail( + result: ReportResult, + opts: ModViewOptions, + ): Promise { const report = await this.report(result) const actionResults = report.resolvedByActionIds.length ? await this.db.db @@ -469,7 +483,7 @@ export class ModerationViews { .execute() : [] const [subject, resolvedByActions] = await Promise.all([ - this.subject(result), + this.subject(result, opts), this.action(actionResults), ]) return { @@ -485,14 +499,17 @@ export class ModerationViews { // Partial view for subjects - async subject(result: SubjectResult): Promise { + async subject( + result: SubjectResult, + opts: ModViewOptions, + ): Promise { let subject: SubjectView if (result.subjectType === 'com.atproto.admin.defs#repoRef') { const repoResult = await this.services .account(this.db) .getAccount(result.subjectDid, true) if (repoResult) { - subject = await this.repo(repoResult) + subject = await this.repo(repoResult, opts) subject.$type = 'com.atproto.admin.defs#repoView' } else { subject = { did: result.subjectDid } @@ -506,7 +523,7 @@ export class ModerationViews { .record(this.db) .getRecord(new AtUri(result.subjectUri), null, true) if (recordResult) { - subject = await this.record(recordResult) + subject = await this.record(recordResult, opts) subject.$type = 'com.atproto.admin.defs#recordView' } else { subject = { uri: result.subjectUri } @@ -610,3 +627,5 @@ type SubjectView = ActionViewDetail['subject'] & ReportViewDetail['subject'] function didFromUri(uri: string) { return new AtUri(uri).host } + +export type ModViewOptions = { includeEmails: boolean } diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index 115d62a4a9f..33b14030074 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -19,6 +19,7 @@ import { MountedAlgos } from '../src/feed-gen/types' const ADMIN_PASSWORD = 'admin-pass' const MODERATOR_PASSWORD = 'moderator-pass' +const TRIAGE_PASSWORD = 'triage-pass' export type CloseFn = () => Promise export type TestServerInfo = { @@ -83,6 +84,7 @@ export const runTestServer = async ( recoveryKey, adminPassword: ADMIN_PASSWORD, moderatorPassword: MODERATOR_PASSWORD, + triagePassword: TRIAGE_PASSWORD, inviteRequired: false, userInviteInterval: null, userInviteEpoch: Date.now(), @@ -180,6 +182,10 @@ export const moderatorAuth = () => { return basicAuth('admin', MODERATOR_PASSWORD) } +export const triageAuth = () => { + return basicAuth('admin', TRIAGE_PASSWORD) +} + const basicAuth = (username: string, password: string) => { return ( 'Basic ' + diff --git a/packages/pds/tests/account.test.ts b/packages/pds/tests/account.test.ts index f8066b392e8..78a769b6e9f 100644 --- a/packages/pds/tests/account.test.ts +++ b/packages/pds/tests/account.test.ts @@ -245,7 +245,7 @@ describe('account', () => { }) it('disallows non-admin moderators to perform email updates', async () => { - const attemptUpdate = agent.api.com.atproto.admin.updateAccountEmail( + const attemptUpdateMod = agent.api.com.atproto.admin.updateAccountEmail( { account: handle, email: 'new@email.com', @@ -255,7 +255,18 @@ describe('account', () => { headers: { authorization: util.moderatorAuth() }, }, ) - await expect(attemptUpdate).rejects.toThrow('Authentication Required') + await expect(attemptUpdateMod).rejects.toThrow('Insufficient privileges') + const attemptUpdateTriage = agent.api.com.atproto.admin.updateAccountEmail( + { + account: handle, + email: 'new@email.com', + }, + { + encoding: 'application/json', + headers: { authorization: util.triageAuth() }, + }, + ) + await expect(attemptUpdateTriage).rejects.toThrow('Insufficient privileges') }) it('disallows duplicate email addresses and handles', async () => { diff --git a/packages/pds/tests/handles.test.ts b/packages/pds/tests/handles.test.ts index 50acddb0656..631ff55fa24 100644 --- a/packages/pds/tests/handles.test.ts +++ b/packages/pds/tests/handles.test.ts @@ -4,7 +4,6 @@ import { SeedClient } from './seeds/client' import basicSeed from './seeds/basic' import * as util from './_util' import { AppContext } from '../src' -import { moderatorAuth } from './_util' // outside of suite so they can be used in mock let alice: string @@ -301,10 +300,21 @@ describe('handles', () => { handle: 'bob-alt.test', }, { - headers: { authorization: moderatorAuth() }, + headers: { authorization: util.moderatorAuth() }, encoding: 'application/json', }, ) - await expect(attempt3).rejects.toThrow('Authentication Required') + await expect(attempt3).rejects.toThrow('Insufficient privileges') + const attempt4 = agent.api.com.atproto.admin.updateAccountHandle( + { + did: bob, + handle: 'bob-alt.test', + }, + { + headers: { authorization: util.triageAuth() }, + encoding: 'application/json', + }, + ) + await expect(attempt4).rejects.toThrow('Insufficient privileges') }) }) diff --git a/packages/pds/tests/moderation.test.ts b/packages/pds/tests/moderation.test.ts index 19bcbee1ad3..17c3ea0d92a 100644 --- a/packages/pds/tests/moderation.test.ts +++ b/packages/pds/tests/moderation.test.ts @@ -1,5 +1,6 @@ import AtpAgent, { ComAtprotoAdminTakeModerationAction } from '@atproto/api' import { AtUri } from '@atproto/uri' +import { BlobNotFoundError } from '@atproto/repo' import { adminAuth, CloseFn, @@ -7,6 +8,7 @@ import { moderatorAuth, runTestServer, TestServerInfo, + triageAuth, } from './_util' import { ImageRef, RecordRef, SeedClient } from './seeds/client' import basicSeed from './seeds/basic' @@ -14,12 +16,12 @@ import { ACKNOWLEDGE, FLAG, TAKEDOWN, + ESCALATE, } from '../src/lexicon/types/com/atproto/admin/defs' import { REASONOTHER, REASONSPAM, } from '../src/lexicon/types/com/atproto/moderation/defs' -import { BlobNotFoundError } from '@atproto/repo' describe('moderation', () => { let server: TestServerInfo @@ -493,13 +495,13 @@ describe('moderation', () => { ) }) - it('supports flagging and acknowledging.', async () => { + it('supports escalating and acknowledging for triage.', async () => { const postRef1 = sc.posts[sc.dids.alice][0].ref const postRef2 = sc.posts[sc.dids.bob][0].ref const { data: action1 } = await agent.api.com.atproto.admin.takeModerationAction( { - action: FLAG, + action: ESCALATE, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef1.uri.toString(), @@ -510,12 +512,12 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: moderatorAuth() }, // As moderator + headers: { authorization: triageAuth() }, }, ) expect(action1).toEqual( expect.objectContaining({ - action: FLAG, + action: ESCALATE, subject: { $type: 'com.atproto.repo.strongRef', uri: postRef1.uriStr, @@ -537,7 +539,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: { authorization: triageAuth() }, }, ) expect(action2).toEqual( @@ -559,7 +561,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: { authorization: triageAuth() }, }, ) await agent.api.com.atproto.admin.reverseModerationAction( @@ -570,7 +572,7 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: adminAuth() }, + headers: { authorization: triageAuth() }, }, ) }) @@ -959,7 +961,7 @@ describe('moderation', () => { await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual(['kittens']) }) - it('does not allow non-admin moderators to label.', async () => { + it('does not allow triage moderators to label.', async () => { const attemptLabel = agent.api.com.atproto.admin.takeModerationAction( { action: ACKNOWLEDGE, @@ -974,32 +976,52 @@ describe('moderation', () => { }, { encoding: 'application/json', - headers: { authorization: moderatorAuth() }, + headers: { authorization: triageAuth() }, }, ) await expect(attemptLabel).rejects.toThrow( - 'Must be an admin to takedown or label content', + 'Must be a full moderator to label content', ) }) it('does not allow non-admin moderators to takedown.', async () => { - const attemptTakedown = agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - createdBy: 'did:example:moderator', - reason: 'Y', - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: sc.dids.bob, + const attemptTakedownMod = + agent.api.com.atproto.admin.takeModerationAction( + { + action: TAKEDOWN, + createdBy: 'did:example:moderator', + reason: 'Y', + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, }, - }, - { - encoding: 'application/json', - headers: { authorization: moderatorAuth() }, - }, + { + encoding: 'application/json', + headers: { authorization: moderatorAuth() }, + }, + ) + await expect(attemptTakedownMod).rejects.toThrow( + 'Must be an admin to perform an account takedown', ) - await expect(attemptTakedown).rejects.toThrow( - 'Must be an admin to takedown or label content', + const attemptTakedownTriage = + agent.api.com.atproto.admin.takeModerationAction( + { + action: TAKEDOWN, + createdBy: 'did:example:moderator', + reason: 'Y', + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, + }, + { + encoding: 'application/json', + headers: { authorization: triageAuth() }, + }, + ) + await expect(attemptTakedownTriage).rejects.toThrow( + 'Must be an admin to perform an account takedown', ) }) diff --git a/packages/pds/tests/views/admin/get-repo.test.ts b/packages/pds/tests/views/admin/get-repo.test.ts index 61ac6ed329c..c0f8d500c90 100644 --- a/packages/pds/tests/views/admin/get-repo.test.ts +++ b/packages/pds/tests/views/admin/get-repo.test.ts @@ -13,6 +13,8 @@ import { CloseFn, adminAuth, TestServerInfo, + moderatorAuth, + triageAuth, } from '../../_util' import { SeedClient } from '../../seeds/client' import basicSeed from '../../seeds/basic' @@ -80,6 +82,25 @@ describe('pds admin get repo view', () => { expect(forSnapshot(result.data)).toMatchSnapshot() }) + it('does not include account emails for triage mods.', async () => { + const { data: admin } = await agent.api.com.atproto.admin.getRepo( + { did: sc.dids.bob }, + { headers: { authorization: adminAuth() } }, + ) + const { data: moderator } = await agent.api.com.atproto.admin.getRepo( + { did: sc.dids.bob }, + { headers: { authorization: moderatorAuth() } }, + ) + const { data: triage } = await agent.api.com.atproto.admin.getRepo( + { did: sc.dids.bob }, + { headers: { authorization: triageAuth() } }, + ) + expect(admin.email).toEqual('bob@test.com') + expect(moderator.email).toEqual('bob@test.com') + expect(triage.email).toBeUndefined() + expect(triage).toEqual({ ...admin, email: undefined }) + }) + it('fails when repo does not exist.', async () => { const promise = agent.api.com.atproto.admin.getRepo( { did: 'did:plc:doesnotexist' }, diff --git a/packages/pds/tests/views/admin/invites.test.ts b/packages/pds/tests/views/admin/invites.test.ts index 90f63cbd1f3..fad766e0c53 100644 --- a/packages/pds/tests/views/admin/invites.test.ts +++ b/packages/pds/tests/views/admin/invites.test.ts @@ -203,7 +203,7 @@ describe('pds admin invite views', () => { }, ) await expect(attemptDisableInvites).rejects.toThrow( - 'Authentication Required', + 'Insufficient privileges', ) }) @@ -215,7 +215,7 @@ describe('pds admin invite views', () => { headers: { authorization: moderatorAuth() }, }, ) - await expect(attemptCreateInvite).rejects.toThrow('Authentication Required') + await expect(attemptCreateInvite).rejects.toThrow('Insufficient privileges') }) it('disables an account from getting additional invite codes', async () => { @@ -255,7 +255,7 @@ describe('pds admin invite views', () => { headers: { authorization: moderatorAuth() }, }, ) - await expect(attempt).rejects.toThrow('Authentication Required') + await expect(attempt).rejects.toThrow('Insufficient privileges') }) it('re-enables an accounts invites', async () => { @@ -285,6 +285,6 @@ describe('pds admin invite views', () => { headers: { authorization: moderatorAuth() }, }, ) - await expect(attempt).rejects.toThrow('Authentication Required') + await expect(attempt).rejects.toThrow('Insufficient privileges') }) }) From 0d3a5559cf26c512fb8ed4553e94bcbb995320fb Mon Sep 17 00:00:00 2001 From: devin ivy Date: Thu, 6 Jul 2023 15:52:16 -0400 Subject: [PATCH 028/237] Do not sequence handle updates in slow path (#1292) * do not sequence handle updates in slow path * build * bail early if invite code doesn't exist --- .github/workflows/build-and-push-pds-aws.yaml | 1 - .../com/atproto/admin/updateAccountHandle.ts | 23 ++++++++++++++--- .../api/com/atproto/identity/updateHandle.ts | 25 ++++++++++++++++--- .../api/com/atproto/server/createAccount.ts | 9 ++++++- packages/pds/src/services/account/index.ts | 16 ++++++++++-- 5 files changed, 64 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-and-push-pds-aws.yaml b/.github/workflows/build-and-push-pds-aws.yaml index 674c9bd44cd..2565b38e432 100644 --- a/.github/workflows/build-and-push-pds-aws.yaml +++ b/.github/workflows/build-and-push-pds-aws.yaml @@ -3,7 +3,6 @@ on: push: branches: - main - - auth-triage-role env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} diff --git a/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts b/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts index 36118cf77a7..2d9346c2c1d 100644 --- a/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts +++ b/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts @@ -2,7 +2,11 @@ import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import * as ident from '@atproto/identifier' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { UserAlreadyExistsError } from '../../../../services/account' +import { + HandleSequenceToken, + UserAlreadyExistsError, +} from '../../../../services/account' +import { httpLogger } from '../../../../logger' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateAccountHandle({ @@ -48,9 +52,10 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Account not found: ${did}`) } - await ctx.db.transaction(async (dbTxn) => { + const seqHandleTok = await ctx.db.transaction(async (dbTxn) => { + let tok: HandleSequenceToken try { - await ctx.services.account(dbTxn).updateHandle(did, handle) + tok = await ctx.services.account(dbTxn).updateHandle(did, handle) } catch (err) { if (err instanceof UserAlreadyExistsError) { throw new InvalidRequestError(`Handle already taken: ${handle}`) @@ -58,7 +63,19 @@ export default function (server: Server, ctx: AppContext) { throw err } await ctx.plcClient.updateHandle(did, ctx.plcRotationKey, handle) + return tok }) + + try { + await ctx.db.transaction(async (dbTxn) => { + await ctx.services.account(dbTxn).sequenceHandle(seqHandleTok) + }) + } catch (err) { + httpLogger.error( + { err, did, handle }, + 'failed to sequence handle update', + ) + } }, }) } diff --git a/packages/pds/src/api/com/atproto/identity/updateHandle.ts b/packages/pds/src/api/com/atproto/identity/updateHandle.ts index acf1c593faa..deb22d0b649 100644 --- a/packages/pds/src/api/com/atproto/identity/updateHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/updateHandle.ts @@ -2,7 +2,11 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import * as ident from '@atproto/identifier' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { UserAlreadyExistsError } from '../../../../services/account' +import { + HandleSequenceToken, + UserAlreadyExistsError, +} from '../../../../services/account' +import { httpLogger } from '../../../../logger' export default function (server: Server, ctx: AppContext) { server.com.atproto.identity.updateHandle({ @@ -43,9 +47,12 @@ export default function (server: Server, ctx: AppContext) { } } - await ctx.db.transaction(async (dbTxn) => { + const seqHandleTok = await ctx.db.transaction(async (dbTxn) => { + let tok: HandleSequenceToken try { - await ctx.services.account(dbTxn).updateHandle(requester, handle) + tok = await ctx.services + .account(dbTxn) + .updateHandle(requester, handle) } catch (err) { if (err instanceof UserAlreadyExistsError) { throw new InvalidRequestError(`Handle already taken: ${handle}`) @@ -53,7 +60,19 @@ export default function (server: Server, ctx: AppContext) { throw err } await ctx.plcClient.updateHandle(requester, ctx.plcRotationKey, handle) + return tok }) + + try { + await ctx.db.transaction(async (dbTxn) => { + await ctx.services.account(dbTxn).sequenceHandle(seqHandleTok) + }) + } catch (err) { + httpLogger.error( + { err, did: requester, handle }, + 'failed to sequence handle update', + ) + } }, }) } diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index dc2c2365e1f..62391034aa3 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -125,13 +125,20 @@ export const ensureCodeIsAvailable = async ( .if(withLock && db.dialect === 'pg', (qb) => qb.forUpdate().skipLocked()) .executeTakeFirst() + if (!invite || invite.disabled) { + throw new InvalidRequestError( + 'Provided invite code not available', + 'InvalidInviteCode', + ) + } + const uses = await db.db .selectFrom('invite_code_use') .select(countAll.as('count')) .where('code', '=', inviteCode) .executeTakeFirstOrThrow() - if (!invite || invite.disabled || invite.availableUses <= uses.count) { + if (invite.availableUses <= uses.count) { throw new InvalidRequestError( 'Provided invite code not available', 'InvalidInviteCode', diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index 5038a91295e..05a512d2f3a 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -149,7 +149,12 @@ export class AccountService { log.info({ handle, email, did }, 'registered user') } - async updateHandle(did: string, handle: string) { + // @NOTE should always be paired with a sequenceHandle(). + // the token output from this method should be passed to sequenceHandle(). + async updateHandle( + did: string, + handle: string, + ): Promise { const res = await this.db.db .updateTable('did_handle') .set({ handle }) @@ -164,7 +169,12 @@ export class AccountService { if (res.numUpdatedRows < 1) { throw new UserAlreadyExistsError() } - const seqEvt = await sequencer.formatSeqHandleUpdate(did, handle) + return { did, handle } + } + + async sequenceHandle(tok: HandleSequenceToken) { + this.db.assertTransaction() + const seqEvt = await sequencer.formatSeqHandleUpdate(tok.did, tok.handle) await sequencer.sequenceEvt(this.db, seqEvt) } @@ -624,3 +634,5 @@ export class ListKeyset extends TimeCidKeyset<{ const matchNamespace = (namespace: string, fullname: string) => { return fullname === namespace || fullname.startsWith(`${namespace}.`) } + +export type HandleSequenceToken = { did: string; handle: string } From d7f87418a5e3356adaa2ad8c84f09c354bcb43ca Mon Sep 17 00:00:00 2001 From: devin ivy Date: Thu, 6 Jul 2023 20:44:52 -0400 Subject: [PATCH 029/237] Tidy repo sub usage of indexing service (#1296) * use single indexing service in repo sub * tidy --- packages/bsky/src/subscription/repo.ts | 31 +++++++++++++------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/packages/bsky/src/subscription/repo.ts b/packages/bsky/src/subscription/repo.ts index db0f84dca8f..ca6385505b5 100644 --- a/packages/bsky/src/subscription/repo.ts +++ b/packages/bsky/src/subscription/repo.ts @@ -10,15 +10,16 @@ import { def, Commit, } from '@atproto/repo' +import { ValidationError } from '@atproto/lexicon' import { OutputSchema as Message } from '../lexicon/types/com/atproto/sync/subscribeRepos' import * as message from '../lexicon/types/com/atproto/sync/subscribeRepos' import { ids, lexicons } from '../lexicon/lexicons' import Database from '../db' import AppContext from '../context' import { Leader } from '../db/leader' +import { IndexingService } from '../services/indexing' import { subLogger } from '../logger' import { ConsecutiveList, LatestQueue, PartitionedQueue } from './util' -import { ValidationError } from '@atproto/lexicon' const METHOD = ids.ComAtprotoSyncSubscribeRepos export const REPO_SUB_ID = 1000 @@ -31,6 +32,7 @@ export class RepoSubscription { destroyed = false lastSeq: number | undefined lastCursor: number | undefined + indexingSvc: IndexingService constructor( public ctx: AppContext, @@ -39,6 +41,7 @@ export class RepoSubscription { public concurrency = Infinity, ) { this.repoQueue = new PartitionedQueue({ concurrency }) + this.indexingSvc = ctx.services.indexing(ctx.db) } async run() { @@ -136,31 +139,29 @@ export class RepoSubscription { } private async handleCommit(msg: message.Commit) { - const { db, services } = this.ctx - const indexingService = services.indexing(db) const indexRecords = async () => { const { root, rootCid, ops } = await getOps(msg) if (msg.tooBig) { - await indexingService.indexRepo(msg.repo, rootCid.toString()) - await indexingService.setCommitLastSeen(root, msg) + await this.indexingSvc.indexRepo(msg.repo, rootCid.toString()) + await this.indexingSvc.setCommitLastSeen(root, msg) return } if (msg.rebase) { - const needsReindex = await indexingService.checkCommitNeedsIndexing( + const needsReindex = await this.indexingSvc.checkCommitNeedsIndexing( root, ) if (needsReindex) { - await indexingService.indexRepo(msg.repo, rootCid.toString()) + await this.indexingSvc.indexRepo(msg.repo, rootCid.toString()) } - await indexingService.setCommitLastSeen(root, msg) + await this.indexingSvc.setCommitLastSeen(root, msg) return } for (const op of ops) { if (op.action === WriteOpAction.Delete) { - await indexingService.deleteRecord(op.uri) + await this.indexingSvc.deleteRecord(op.uri) } else { try { - await indexingService.indexRecord( + await this.indexingSvc.indexRecord( op.uri, op.cid, op.record, @@ -193,23 +194,21 @@ export class RepoSubscription { } } } - await indexingService.setCommitLastSeen(root, msg) + await this.indexingSvc.setCommitLastSeen(root, msg) } const results = await Promise.allSettled([ indexRecords(), - indexingService.indexHandle(msg.repo, msg.time), + this.indexingSvc.indexHandle(msg.repo, msg.time), ]) handleAllSettledErrors(results) } private async handleUpdateHandle(msg: message.Handle) { - const { db, services } = this.ctx - await services.indexing(db).indexHandle(msg.did, msg.time, true) + await this.indexingSvc.indexHandle(msg.did, msg.time, true) } private async handleTombstone(msg: message.Tombstone) { - const { db, services } = this.ctx - await services.indexing(db).tombstoneActor(msg.did) + await this.indexingSvc.tombstoneActor(msg.did) } private async handleCursor(seq: number) { From b92288cfcacf161ebd0664a526371b6d114be1dc Mon Sep 17 00:00:00 2001 From: devin ivy Date: Thu, 6 Jul 2023 23:31:11 -0400 Subject: [PATCH 030/237] Sync-up appview feed date threshold w/ pds (#1286) sync-up appview feed date threshold w/ pds --- packages/bsky/src/api/app/bsky/util/feed.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bsky/src/api/app/bsky/util/feed.ts b/packages/bsky/src/api/app/bsky/util/feed.ts index 9e85f4e974f..384f67e32e9 100644 --- a/packages/bsky/src/api/app/bsky/util/feed.ts +++ b/packages/bsky/src/api/app/bsky/util/feed.ts @@ -12,7 +12,7 @@ export class FeedKeyset extends TimeCidKeyset { } // For users with sparse feeds, avoid scanning more than one week for a single page -export const getFeedDateThreshold = (from: string | undefined, days = 3) => { +export const getFeedDateThreshold = (from: string | undefined, days = 1) => { const timelineDateThreshold = from ? new Date(from) : new Date() timelineDateThreshold.setDate(timelineDateThreshold.getDate() - days) return timelineDateThreshold.toISOString() From 08dc2b725dac41c83c8d5a5c40889ba0f20d59f0 Mon Sep 17 00:00:00 2001 From: Ansh Date: Fri, 7 Jul 2023 11:49:02 -0700 Subject: [PATCH 031/237] Pagination on getPopularFeedGenerators (#1293) * add temporary page-based pagination * use cursor & limit for pagination * tests * add to appview --------- Co-authored-by: dholms --- .../unspecced/getPopularFeedGenerators.json | 8 ++ packages/api/src/client/lexicons.ts | 17 +++++ .../unspecced/getPopularFeedGenerators.ts | 6 +- .../unspecced/getPopularFeedGenerators.ts | 73 +++++++++++++----- packages/bsky/src/lexicon/lexicons.ts | 17 +++++ .../unspecced/getPopularFeedGenerators.ts | 6 +- packages/bsky/tests/feed-generation.test.ts | 22 ++++++ .../src/app-view/api/app/bsky/unspecced.ts | 74 +++++++++++++------ packages/pds/src/lexicon/lexicons.ts | 17 +++++ .../unspecced/getPopularFeedGenerators.ts | 6 +- packages/pds/tests/feed-generation.test.ts | 27 ++++++- 11 files changed, 226 insertions(+), 47 deletions(-) diff --git a/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json b/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json index 4bc46acbee6..5b1bce07cc8 100644 --- a/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json +++ b/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json @@ -5,12 +5,20 @@ "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"} + } + }, "output": { "encoding": "application/json", "schema": { "type": "object", "required": ["feeds"], "properties": { + "cursor": {"type": "string"}, "feeds": { "type": "array", "items": {"type": "ref", "ref": "app.bsky.feed.defs#generatorView"} diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index f2adcb93c69..68186fdb49e 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -6285,12 +6285,29 @@ export const schemaDict = { 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', + }, + }, + }, output: { encoding: 'application/json', schema: { type: 'object', required: ['feeds'], properties: { + cursor: { + type: 'string', + }, feeds: { type: 'array', items: { diff --git a/packages/api/src/client/types/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/api/src/client/types/app/bsky/unspecced/getPopularFeedGenerators.ts index e6d15b062f4..d4764648085 100644 --- a/packages/api/src/client/types/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/api/src/client/types/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -8,11 +8,15 @@ import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' import * as AppBskyFeedDefs from '../feed/defs' -export interface QueryParams {} +export interface QueryParams { + limit?: number + cursor?: string +} export type InputSchema = undefined export interface OutputSchema { + cursor?: string feeds: AppBskyFeedDefs.GeneratorView[] [k: string]: unknown } diff --git a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index 303fed257cb..dff5965f930 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -1,57 +1,90 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { countAll } from '../../../../db/util' +import { GenericKeyset, paginate } from '../../../../db/pagination' +import { InvalidRequestError } from '@atproto/xrpc-server' +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, - handler: async ({ auth }) => { + handler: async ({ auth, params }) => { + const { limit, cursor } = params const requester = auth.credentials.did const db = ctx.db.db const { ref } = db.dynamic const feedService = ctx.services.feed(ctx.db) - const mostPopularFeeds = await ctx.db.db + const inner = ctx.db.db .selectFrom('feed_generator') .select([ 'uri', + 'cid', ctx.db.db .selectFrom('like') .whereRef('like.subject', '=', ref('feed_generator.uri')) .select(countAll.as('count')) .as('likeCount'), ]) - .orderBy('likeCount', 'desc') - .orderBy('cid', 'desc') - .limit(50) - .execute() - const genViews = await feedService.getFeedGeneratorViews( - mostPopularFeeds.map((feed) => feed.uri), + let builder = ctx.db.db.selectFrom(inner.as('feed_gens')).selectAll() + + const keyset = new LikeCountKeyset(ref('likeCount'), ref('cid')) + builder = paginate(builder, { limit, cursor, keyset, direction: 'desc' }) + + const res = await builder.execute() + + const genInfos = await feedService.getFeedGeneratorViews( + res.map((feed) => feed.uri), requester, ) - const genList = Object.values(genViews) - const creators = genList.map((gen) => gen.creator) + const creators = Object.values(genInfos).map((gen) => gen.creator) const profiles = await feedService.getActorViews(creators, requester) - const feedViews = genList.map((gen) => - feedService.views.formatFeedGeneratorView(gen, profiles), - ) + const genViews: GeneratorView[] = [] + for (const row of res) { + const gen = genInfos[row.uri] + if (!gen) continue + const view = feedService.views.formatFeedGeneratorView(gen, profiles) + genViews.push(view) + } return { encoding: 'application/json', body: { - feeds: feedViews.sort((feedA, feedB) => { - const likeA = feedA.likeCount ?? 0 - const likeB = feedB.likeCount ?? 0 - const likeDiff = likeB - likeA - if (likeDiff !== 0) return likeDiff - return feedB.cid.localeCompare(feedA.cid) - }), + cursor: keyset.packFromResult(res), + feeds: genViews, }, } }, }) } + +type Result = { likeCount: number; cid: string } +type LabeledResult = { primary: number; secondary: string } +export class LikeCountKeyset extends GenericKeyset { + labelResult(result: Result) { + return { + primary: result.likeCount, + secondary: result.cid, + } + } + labeledResultToCursor(labeled: LabeledResult) { + return { + primary: labeled.primary.toString(), + secondary: labeled.secondary, + } + } + cursorToLabeledResult(cursor: { primary: string; secondary: string }) { + const likes = parseInt(cursor.primary, 10) + if (isNaN(likes)) { + throw new InvalidRequestError('Malformed cursor') + } + return { + primary: likes, + secondary: cursor.secondary, + } + } +} diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index f2adcb93c69..68186fdb49e 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -6285,12 +6285,29 @@ export const schemaDict = { 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', + }, + }, + }, output: { encoding: 'application/json', schema: { type: 'object', required: ['feeds'], properties: { + cursor: { + type: 'string', + }, feeds: { type: 'array', items: { diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts index 981f7d92519..0b53114fe94 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -9,11 +9,15 @@ import { CID } from 'multiformats/cid' import { HandlerAuth } from '@atproto/xrpc-server' import * as AppBskyFeedDefs from '../feed/defs' -export interface QueryParams {} +export interface QueryParams { + limit: number + cursor?: string +} export type InputSchema = undefined export interface OutputSchema { + cursor?: string feeds: AppBskyFeedDefs.GeneratorView[] [k: string]: unknown } diff --git a/packages/bsky/tests/feed-generation.test.ts b/packages/bsky/tests/feed-generation.test.ts index 97d16c3e156..950203bc877 100644 --- a/packages/bsky/tests/feed-generation.test.ts +++ b/packages/bsky/tests/feed-generation.test.ts @@ -330,6 +330,28 @@ describe('feed generation', () => { expect(resEven.data.feeds.map((f) => f.likeCount)).toEqual([2, 0, 0, 0]) expect(resEven.data.feeds.map((f) => f.uri)).not.toContain(feedUriPrime) // taken-down }) + + it('paginates', async () => { + const resFull = + await agent.api.app.bsky.unspecced.getPopularFeedGenerators( + {}, + { headers: await network.serviceHeaders(sc.dids.bob) }, + ) + + const resOne = + await agent.api.app.bsky.unspecced.getPopularFeedGenerators( + { limit: 2 }, + { headers: await network.serviceHeaders(sc.dids.bob) }, + ) + const resTwo = + await agent.api.app.bsky.unspecced.getPopularFeedGenerators( + { cursor: resOne.data.cursor }, + { headers: await network.serviceHeaders(sc.dids.bob) }, + ) + expect([...resOne.data.feeds, ...resTwo.data.feeds]).toEqual( + resFull.data.feeds, + ) + }) }) describe('getFeed', () => { diff --git a/packages/pds/src/app-view/api/app/bsky/unspecced.ts b/packages/pds/src/app-view/api/app/bsky/unspecced.ts index 5688549fbe7..56129f9e904 100644 --- a/packages/pds/src/app-view/api/app/bsky/unspecced.ts +++ b/packages/pds/src/app-view/api/app/bsky/unspecced.ts @@ -1,12 +1,14 @@ import { Server } from '../../../../lexicon' import { FeedKeyset } from './util/feed' -import { paginate } from '../../../../db/pagination' +import { GenericKeyset, paginate } from '../../../../db/pagination' import AppContext from '../../../../context' import { FeedRow } from '../../../services/feed' import { isPostView } from '../../../../lexicon/types/app/bsky/feed/defs' import { NotEmptyArray } from '@atproto/common' import { isViewRecord } from '../../../../lexicon/types/app/bsky/embed/record' import { countAll, valuesList } from '../../../../db/util' +import { InvalidRequestError } from '@atproto/xrpc-server' +import { GeneratorView } from '@atproto/api/src/client/types/app/bsky/feed/defs' const NO_WHATS_HOT_LABELS: NotEmptyArray = [ '!no-promote', @@ -126,52 +128,82 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.unspecced.getPopularFeedGenerators({ auth: ctx.accessVerifier, - handler: async ({ auth }) => { + handler: async ({ auth, params }) => { const requester = auth.credentials.did const db = ctx.db.db + const { limit, cursor } = params const { ref } = db.dynamic const feedService = ctx.services.appView.feed(ctx.db) - const mostPopularFeeds = await ctx.db.db + const inner = ctx.db.db .selectFrom('feed_generator') .select([ 'uri', + 'cid', ctx.db.db .selectFrom('like') .whereRef('like.subject', '=', ref('feed_generator.uri')) .select(countAll.as('count')) .as('likeCount'), ]) - .orderBy('likeCount', 'desc') - .orderBy('cid', 'desc') - .limit(50) - .execute() - const genViews = await feedService.getFeedGeneratorInfos( - mostPopularFeeds.map((feed) => feed.uri), + let builder = ctx.db.db.selectFrom(inner.as('feed_gens')).selectAll() + + const keyset = new LikeCountKeyset(ref('likeCount'), ref('cid')) + builder = paginate(builder, { limit, cursor, keyset, direction: 'desc' }) + + const res = await builder.execute() + + const genInfos = await feedService.getFeedGeneratorInfos( + res.map((feed) => feed.uri), requester, ) - const genList = Object.values(genViews) - const creators = genList.map((gen) => gen.creator) + const creators = Object.values(genInfos).map((gen) => gen.creator) const profiles = await feedService.getActorInfos(creators, requester) - const feedViews = genList.map((gen) => - feedService.views.formatFeedGeneratorView(gen, profiles), - ) + const genViews: GeneratorView[] = [] + for (const row of res) { + const gen = genInfos[row.uri] + if (!gen) continue + const view = feedService.views.formatFeedGeneratorView(gen, profiles) + genViews.push(view) + } return { encoding: 'application/json', body: { - feeds: feedViews.sort((feedA, feedB) => { - const likeA = feedA.likeCount ?? 0 - const likeB = feedB.likeCount ?? 0 - const likeDiff = likeB - likeA - if (likeDiff !== 0) return likeDiff - return feedB.cid.localeCompare(feedA.cid) - }), + cursor: keyset.packFromResult(res), + feeds: genViews, }, } }, }) } + +type Result = { likeCount: number; cid: string } +type LabeledResult = { primary: number; secondary: string } +export class LikeCountKeyset extends GenericKeyset { + labelResult(result: Result) { + return { + primary: result.likeCount, + secondary: result.cid, + } + } + labeledResultToCursor(labeled: LabeledResult) { + return { + primary: labeled.primary.toString(), + secondary: labeled.secondary, + } + } + cursorToLabeledResult(cursor: { primary: string; secondary: string }) { + const likes = parseInt(cursor.primary, 10) + if (isNaN(likes)) { + throw new InvalidRequestError('Malformed cursor') + } + return { + primary: likes, + secondary: cursor.secondary, + } + } +} diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index f2adcb93c69..68186fdb49e 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -6285,12 +6285,29 @@ export const schemaDict = { 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', + }, + }, + }, output: { encoding: 'application/json', schema: { type: 'object', required: ['feeds'], properties: { + cursor: { + type: 'string', + }, feeds: { type: 'array', items: { diff --git a/packages/pds/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/pds/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts index 981f7d92519..0b53114fe94 100644 --- a/packages/pds/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/pds/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -9,11 +9,15 @@ import { CID } from 'multiformats/cid' import { HandlerAuth } from '@atproto/xrpc-server' import * as AppBskyFeedDefs from '../feed/defs' -export interface QueryParams {} +export interface QueryParams { + limit: number + cursor?: string +} export type InputSchema = undefined export interface OutputSchema { + cursor?: string feeds: AppBskyFeedDefs.GeneratorView[] [k: string]: unknown } diff --git a/packages/pds/tests/feed-generation.test.ts b/packages/pds/tests/feed-generation.test.ts index caa1f38423c..b8891ae0ef2 100644 --- a/packages/pds/tests/feed-generation.test.ts +++ b/packages/pds/tests/feed-generation.test.ts @@ -307,13 +307,34 @@ describe('feed generation', () => { describe('getPopularFeedGenerators', () => { it('gets popular feed generators', async () => { - const resEven = + const res = await agent.api.app.bsky.unspecced.getPopularFeedGenerators( + {}, + { headers: sc.getHeaders(sc.dids.bob) }, + ) + expect(res.data.feeds.map((f) => f.likeCount)).toEqual([2, 0, 0, 0]) + expect(res.data.feeds.map((f) => f.uri)).not.toContain(feedUriPrime) // taken-down + }) + + it('paginates', async () => { + const resFull = await agent.api.app.bsky.unspecced.getPopularFeedGenerators( {}, { headers: sc.getHeaders(sc.dids.bob) }, ) - expect(resEven.data.feeds.map((f) => f.likeCount)).toEqual([2, 0, 0, 0]) - expect(resEven.data.feeds.map((f) => f.uri)).not.toContain(feedUriPrime) // taken-down + + const resOne = + await agent.api.app.bsky.unspecced.getPopularFeedGenerators( + { limit: 2 }, + { headers: sc.getHeaders(sc.dids.bob) }, + ) + const resTwo = + await agent.api.app.bsky.unspecced.getPopularFeedGenerators( + { cursor: resOne.data.cursor }, + { headers: sc.getHeaders(sc.dids.bob) }, + ) + expect([...resOne.data.feeds, ...resTwo.data.feeds]).toEqual( + resFull.data.feeds, + ) }) }) From 2e52f382a146ad51d5a6bf9c7fe94869a7765576 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Fri, 7 Jul 2023 14:49:47 -0400 Subject: [PATCH 032/237] Perform pessimistic handle check when updating handle (#1300) perform pessimistic handle check when updating handle --- .../pds/src/api/com/atproto/identity/updateHandle.ts | 8 ++++++++ packages/pds/src/services/account/index.ts | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/packages/pds/src/api/com/atproto/identity/updateHandle.ts b/packages/pds/src/api/com/atproto/identity/updateHandle.ts index deb22d0b649..0119ae79810 100644 --- a/packages/pds/src/api/com/atproto/identity/updateHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/updateHandle.ts @@ -47,6 +47,14 @@ export default function (server: Server, ctx: AppContext) { } } + // Pessimistic check to handle spam: also enforced by updateHandle() and the db. + const available = await ctx.services + .account(ctx.db) + .isHandleAvailable(handle) + if (!available) { + throw new InvalidRequestError(`Handle already taken: ${handle}`) + } + const seqHandleTok = await ctx.db.transaction(async (dbTxn) => { let tok: HandleSequenceToken try { diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index 05a512d2f3a..c1f267fd6b0 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -160,6 +160,7 @@ export class AccountService { .set({ handle }) .where('did', '=', did) .whereNotExists( + // @NOTE see also condition in isHandleAvailable() this.db.db .selectFrom('did_handle') .where('handle', '=', handle) @@ -178,6 +179,16 @@ export class AccountService { await sequencer.sequenceEvt(this.db, seqEvt) } + async isHandleAvailable(handle: string) { + // @NOTE see also condition in updateHandle() + const found = await this.db.db + .selectFrom('did_handle') + .where('handle', '=', handle) + .select('handle') + .executeTakeFirst() + return !found + } + async updateEmail(did: string, email: string) { await this.db.db .updateTable('user_account') From 5e34a8bfc6c255e00c89cd42721ede9f4460313c Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Fri, 7 Jul 2023 13:52:29 -0500 Subject: [PATCH 033/237] Add websocket heartbeat (#1294) * refactor & add heartbeat to websockets * comment * testing * tidy * pr feedback * pass ws ref to startHeartbeat * tidy * end interval on ws close * rm only * build branch * dont build branch --- packages/xrpc-server/src/stream/stream.ts | 35 ++-- .../xrpc-server/src/stream/subscription.ts | 128 +++------------ .../src/stream/websocket-keepalive.ts | 151 ++++++++++++++++++ .../xrpc-server/tests/subscriptions.test.ts | 58 ++++++- 4 files changed, 253 insertions(+), 119 deletions(-) create mode 100644 packages/xrpc-server/src/stream/websocket-keepalive.ts diff --git a/packages/xrpc-server/src/stream/stream.ts b/packages/xrpc-server/src/stream/stream.ts index 356d8835caf..d2475365e0d 100644 --- a/packages/xrpc-server/src/stream/stream.ts +++ b/packages/xrpc-server/src/stream/stream.ts @@ -1,26 +1,39 @@ import { XRPCError, ResponseType } from '@atproto/xrpc' import { DuplexOptions } from 'stream' import { createWebSocketStream, WebSocket } from 'ws' -import { Frame } from './frames' +import { Frame, MessageFrame } from './frames' -export async function* byFrame(ws: WebSocket, options?: DuplexOptions) { - const wsStream = createWebSocketStream(ws, { +export function streamByteChunks(ws: WebSocket, options?: DuplexOptions) { + return createWebSocketStream(ws, { ...options, readableObjectMode: true, // Ensures frame bytes don't get buffered/combined together }) +} + +export async function* byFrame(ws: WebSocket, options?: DuplexOptions) { + const wsStream = streamByteChunks(ws, options) for await (const chunk of wsStream) { yield Frame.fromBytes(chunk) } } export async function* byMessage(ws: WebSocket, options?: DuplexOptions) { - for await (const frame of byFrame(ws, options)) { - if (frame.isMessage()) { - yield frame - } else if (frame.isError()) { - throw new XRPCError(-1, frame.code, frame.message) - } else { - throw new XRPCError(ResponseType.Unknown, undefined, 'Unknown frame type') - } + const wsStream = streamByteChunks(ws, options) + for await (const chunk of wsStream) { + const msg = ensureChunkIsMessage(chunk) + yield msg + } +} + +export function ensureChunkIsMessage(chunk: Uint8Array): MessageFrame { + const frame = Frame.fromBytes(chunk) + if (frame.isMessage()) { + return frame + } else if (frame.isError()) { + // @TODO work -1 error code into XRPCError + // @ts-ignore + throw new XRPCError(-1, frame.code, frame.message) + } else { + throw new XRPCError(ResponseType.Unknown, undefined, 'Unknown frame type') } } diff --git a/packages/xrpc-server/src/stream/subscription.ts b/packages/xrpc-server/src/stream/subscription.ts index c4d6c9eec0e..50fdb804017 100644 --- a/packages/xrpc-server/src/stream/subscription.ts +++ b/packages/xrpc-server/src/stream/subscription.ts @@ -1,7 +1,6 @@ -import { wait } from '@atproto/common' -import { WebSocket, ClientOptions } from 'ws' -import { byMessage } from './stream' -import { CloseCode, DisconnectError } from './types' +import { ClientOptions } from 'ws' +import { WebSocketKeepAlive } from './websocket-keepalive' +import { ensureChunkIsMessage } from './stream' export class Subscription { constructor( @@ -9,6 +8,7 @@ export class Subscription { service: string method: string maxReconnectSeconds?: number + heartbeatIntervalMs?: number signal?: AbortSignal validate: (obj: unknown) => T | undefined onReconnectError?: ( @@ -24,107 +24,31 @@ export class Subscription { ) {} async *[Symbol.asyncIterator](): AsyncGenerator { - let initialSetup = true - let reconnects: number | null = null - const maxReconnectMs = 1000 * (this.opts.maxReconnectSeconds ?? 64) - while (true) { - if (reconnects !== null) { - const duration = initialSetup - ? Math.min(1000, maxReconnectMs) - : backoffMs(reconnects++, maxReconnectMs) - await wait(duration) - } - const ws = await this.getSocket() - const ac = new AbortController() - if (this.opts.signal) { - forwardSignal(this.opts.signal, ac) + const ws = new WebSocketKeepAlive({ + ...this.opts, + getUrl: async () => { + const params = (await this.opts.getParams?.()) ?? {} + const query = encodeQueryParams(params) + return `${this.opts.service}/xrpc/${this.opts.method}?${query}` + }, + }) + for await (const chunk of ws) { + const message = await ensureChunkIsMessage(chunk) + const t = message.header.t + const clone = message.body !== undefined ? { ...message.body } : undefined + if (clone !== undefined && t !== undefined) { + clone['$type'] = t.startsWith('#') ? this.opts.method + t : t } - ws.once('open', () => { - initialSetup = false - reconnects = 0 - }) - ws.once('close', (code, reason) => { - if (code === CloseCode.Abnormal) { - // Forward into an error to distinguish from a clean close - ac.abort( - new AbnormalCloseError(`Abnormal ws close: ${reason.toString()}`), - ) - } - }) - try { - const cancelable = { signal: ac.signal } - for await (const message of byMessage(ws, cancelable)) { - const t = message.header.t - const clone = - message.body !== undefined ? { ...message.body } : undefined - if (clone !== undefined && t !== undefined) { - clone['$type'] = t.startsWith('#') ? this.opts.method + t : t - } - const result = this.opts.validate(clone) - if (result !== undefined) { - yield result - } - } - } catch (_err) { - const err = _err?.['code'] === 'ABORT_ERR' ? _err['cause'] : _err - if (err instanceof DisconnectError) { - // We cleanly end the connection - ws.close(err.wsCode) - break - } - ws.close() // No-ops if already closed or closing - if (isReconnectable(err)) { - reconnects ??= 0 // Never reconnect with a null - this.opts.onReconnectError?.(err, reconnects, initialSetup) - continue - } else { - throw err - } + const result = this.opts.validate(clone) + if (result !== undefined) { + yield result } - break // Other side cleanly ended stream and disconnected } } - - private async getSocket() { - const params = (await this.opts.getParams?.()) ?? {} - const query = encodeQueryParams(params) - const url = `${this.opts.service}/xrpc/${this.opts.method}?${query}` - return new WebSocket(url, this.opts) - } } export default Subscription -class AbnormalCloseError extends Error { - code = 'EWSABNORMALCLOSE' -} - -function isReconnectable(err: unknown): boolean { - // Network errors are reconnectable. - // AuthenticationRequired and InvalidRequest XRPCErrors are not reconnectable. - // @TODO method-specific XRPCErrors may be reconnectable, need to consider. Receiving - // an invalid message is not current reconnectable, but the user can decide to skip them. - if (!err || typeof err['code'] !== 'string') return false - return networkErrorCodes.includes(err['code']) -} - -const networkErrorCodes = [ - 'EWSABNORMALCLOSE', - 'ECONNRESET', - 'ECONNREFUSED', - 'ECONNABORTED', - 'EPIPE', - 'ETIMEDOUT', - 'ECANCELED', -] - -function backoffMs(n: number, maxMs: number) { - const baseSec = Math.pow(2, n) // 1, 2, 4, ... - const randSec = Math.random() - 0.5 // Random jitter between -.5 and .5 seconds - const ms = 1000 * (baseSec + randSec) - return Math.min(ms, maxMs) -} - function encodeQueryParams(obj: Record): string { const params = new URLSearchParams() Object.entries(obj).forEach(([key, value]) => { @@ -163,13 +87,3 @@ function encodeQueryParam(value: unknown): string | string[] { } throw new Error(`Cannot encode ${typeof value}s into query params`) } - -function forwardSignal(signal: AbortSignal, ac: AbortController) { - if (signal.aborted) { - return ac.abort(signal.reason) - } else { - signal.addEventListener('abort', () => ac.abort(signal.reason), { - signal: ac.signal, - }) - } -} diff --git a/packages/xrpc-server/src/stream/websocket-keepalive.ts b/packages/xrpc-server/src/stream/websocket-keepalive.ts new file mode 100644 index 00000000000..696a058df5c --- /dev/null +++ b/packages/xrpc-server/src/stream/websocket-keepalive.ts @@ -0,0 +1,151 @@ +import { SECOND, wait } from '@atproto/common' +import { WebSocket, ClientOptions } from 'ws' +import { streamByteChunks } from './stream' +import { CloseCode, DisconnectError } from './types' + +export class WebSocketKeepAlive { + public ws: WebSocket | null = null + public initialSetup = true + public reconnects: number | null = null + + constructor( + public opts: ClientOptions & { + getUrl: () => Promise + maxReconnectSeconds?: number + signal?: AbortSignal + heartbeatIntervalMs?: number + onReconnectError?: ( + error: unknown, + n: number, + initialSetup: boolean, + ) => void + }, + ) {} + + async *[Symbol.asyncIterator](): AsyncGenerator { + const maxReconnectMs = 1000 * (this.opts.maxReconnectSeconds ?? 64) + while (true) { + if (this.reconnects !== null) { + const duration = this.initialSetup + ? Math.min(1000, maxReconnectMs) + : backoffMs(this.reconnects++, maxReconnectMs) + await wait(duration) + } + const url = await this.opts.getUrl() + this.ws = new WebSocket(url, this.opts) + const ac = new AbortController() + if (this.opts.signal) { + forwardSignal(this.opts.signal, ac) + } + this.ws.once('open', () => { + this.initialSetup = false + this.reconnects = 0 + if (this.ws) { + this.startHeartbeat(this.ws) + } + }) + this.ws.once('close', (code, reason) => { + if (code === CloseCode.Abnormal) { + // Forward into an error to distinguish from a clean close + ac.abort( + new AbnormalCloseError(`Abnormal ws close: ${reason.toString()}`), + ) + } + }) + + try { + const wsStream = streamByteChunks(this.ws, { signal: ac.signal }) + for await (const chunk of wsStream) { + yield chunk + } + } catch (_err) { + const err = _err?.['code'] === 'ABORT_ERR' ? _err['cause'] : _err + if (err instanceof DisconnectError) { + // We cleanly end the connection + this.ws?.close(err.wsCode) + break + } + this.ws?.close() // No-ops if already closed or closing + if (isReconnectable(err)) { + this.reconnects ??= 0 // Never reconnect with a null + this.opts.onReconnectError?.(err, this.reconnects, this.initialSetup) + continue + } else { + throw err + } + } + break // Other side cleanly ended stream and disconnected + } + } + + startHeartbeat(ws: WebSocket) { + let isAlive = true + let heartbeatInterval: NodeJS.Timer | null = null + + const checkAlive = () => { + if (!isAlive) { + return ws.terminate() + } + isAlive = false // expect websocket to no longer be alive unless we receive a "pong" within the interval + ws.ping() + } + + checkAlive() + heartbeatInterval = setInterval( + checkAlive, + this.opts.heartbeatIntervalMs ?? 10 * SECOND, + ) + + ws.on('pong', () => { + isAlive = true + }) + ws.once('close', () => { + if (heartbeatInterval) { + clearInterval(heartbeatInterval) + heartbeatInterval = null + } + }) + } +} + +export default WebSocketKeepAlive + +class AbnormalCloseError extends Error { + code = 'EWSABNORMALCLOSE' +} + +function isReconnectable(err: unknown): boolean { + // Network errors are reconnectable. + // AuthenticationRequired and InvalidRequest XRPCErrors are not reconnectable. + // @TODO method-specific XRPCErrors may be reconnectable, need to consider. Receiving + // an invalid message is not current reconnectable, but the user can decide to skip them. + if (!err || typeof err['code'] !== 'string') return false + return networkErrorCodes.includes(err['code']) +} + +const networkErrorCodes = [ + 'EWSABNORMALCLOSE', + 'ECONNRESET', + 'ECONNREFUSED', + 'ECONNABORTED', + 'EPIPE', + 'ETIMEDOUT', + 'ECANCELED', +] + +function backoffMs(n: number, maxMs: number) { + const baseSec = Math.pow(2, n) // 1, 2, 4, ... + const randSec = Math.random() - 0.5 // Random jitter between -.5 and .5 seconds + const ms = 1000 * (baseSec + randSec) + return Math.min(ms, maxMs) +} + +function forwardSignal(signal: AbortSignal, ac: AbortController) { + if (signal.aborted) { + return ac.abort(signal.reason) + } else { + signal.addEventListener('abort', () => ac.abort(signal.reason), { + signal: ac.signal, + }) + } +} diff --git a/packages/xrpc-server/tests/subscriptions.test.ts b/packages/xrpc-server/tests/subscriptions.test.ts index ca0e46c4386..13b0301ca87 100644 --- a/packages/xrpc-server/tests/subscriptions.test.ts +++ b/packages/xrpc-server/tests/subscriptions.test.ts @@ -1,5 +1,5 @@ import * as http from 'http' -import { WebSocket, createWebSocketStream } from 'ws' +import { WebSocket, WebSocketServer, createWebSocketStream } from 'ws' import getPort from 'get-port' import { wait } from '@atproto/common' import { byFrame, MessageFrame, ErrorFrame, Frame, Subscription } from '../src' @@ -95,6 +95,7 @@ describe('Subscriptions', () => { server.streamMethod('io.example.streamTwo', async function* ({ params }) { const countdown = Number(params.countdown ?? 0) for (let i = countdown; i >= 0; i--) { + await wait(200) yield { $type: i % 2 === 0 ? '#even' : 'io.example.streamTwo#odd', count: i, @@ -344,4 +345,59 @@ describe('Subscriptions', () => { ]) }) }) + + it('uses a heartbeat to reconnect if a connection is dropped', async () => { + // we run a server that, on first connection, pauses for longer than the heartbeat interval (doesn't return "pong"s) + // on second connection, it returns a message frame and then closes + const port = await getPort() + const server = new WebSocketServer({ port }) + let firstConnection = true + let firstWasClosed = false + server.on('connection', async (socket) => { + if (firstConnection === true) { + firstConnection = false + socket.pause() + await wait(600) + // shouldn't send this message because the socket would be closed + const frame = new ErrorFrame({ + error: 'AuthenticationRequired', + message: 'Authentication Required', + }) + socket.send(frame.toBytes(), { binary: true }, (err) => { + if (err) throw err + socket.close(xrpcServer.CloseCode.Normal) + }) + socket.on('close', () => { + firstWasClosed = true + }) + } else { + const frame = new MessageFrame({ count: 1 }) + socket.send(frame.toBytes(), { binary: true }, (err) => { + if (err) throw err + socket.close(xrpcServer.CloseCode.Normal) + }) + } + }) + + const subscription = new Subscription({ + service: `ws://localhost:${port}`, + method: '', + heartbeatIntervalMs: 500, + validate: (obj) => { + return lex.assertValidXrpcMessage<{ count: number }>( + 'io.example.streamOne', + obj, + ) + }, + }) + + const messages: { count: number }[] = [] + for await (const msg of subscription) { + messages.push(msg) + } + + expect(messages).toEqual([{ count: 1 }]) + expect(firstWasClosed).toBe(true) + server.close() + }) }) From c78d30f363eecee9f695a2e84e0274ffa5880f44 Mon Sep 17 00:00:00 2001 From: David Buchanan Date: Wed, 12 Jul 2023 07:00:26 +0100 Subject: [PATCH 034/237] hotfix: prevent user-supplied rkey on posts with createRecord (#1313) * prevent user-supplied rkey on posts with createRecord * allow empty-string rkey parameter Co-authored-by: devin ivy --------- Co-authored-by: devin ivy --- packages/pds/src/api/com/atproto/repo/createRecord.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/pds/src/api/com/atproto/repo/createRecord.ts b/packages/pds/src/api/com/atproto/repo/createRecord.ts index 978a3945253..0f28f8cbb4f 100644 --- a/packages/pds/src/api/com/atproto/repo/createRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/createRecord.ts @@ -33,6 +33,11 @@ export default function (server: Server, ctx: AppContext) { 'Unvalidated writes are not yet supported.', ) } + if (collection === ids.AppBskyFeedPost && rkey) { + throw new InvalidRequestError( + 'Custom rkeys for post records are not currently supported.', + ) + } const swapCommitCid = swapCommit ? CID.parse(swapCommit) : undefined From 3d47eed592fb008ceaec8352dcc5a33cdf89df2d Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 12 Jul 2023 23:38:15 -0500 Subject: [PATCH 035/237] add slurs to reserved words (#1318) * add slurs to reserved words (#1314) * Update reserved.ts Add slurs to reserved words * Update reserved.ts fix typo * Update reserved.ts to clean up the slur list * linting * pluralise --------- Co-authored-by: jess --- packages/identifier/src/reserved.ts | 86 +++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/packages/identifier/src/reserved.ts b/packages/identifier/src/reserved.ts index c49c85f5378..c924831a966 100644 --- a/packages/identifier/src/reserved.ts +++ b/packages/identifier/src/reserved.ts @@ -1047,10 +1047,96 @@ const famousAccounts = [ 'zerohora', ] +// Needs additional work to add more words that should be caught and regex added to catch common variations +const slurs = [ + 'chinaman', + 'chinamen', + 'chink', + 'chinks', + 'coolie', + 'coolies', + 'coon', + 'coons', + 'golliwog', + 'golliwogs', + 'gook', + 'gooks', + 'gyp', + 'gyps', + 'half-breed', + 'half-breeds', + 'halfbreed', + 'halfbreeds', + 'heeb', + 'heebs', + 'jap', + 'japs', + 'kaffer', + 'kaffers', + 'kaffir', + 'kaffirs', + 'kaffre', + 'kaffres', + 'kafir', + 'kafirs', + 'kike', + 'kikes', + 'kraut', + 'krauts', + 'negress', + 'negro', + 'negros', + 'nig', + 'nigs', + 'nig-nog', + 'nig-nogs', + 'nigga', + 'niggas', + 'nigger', + 'niggers', + 'nigguh', + 'nigguhs', + 'pajeet', + 'pajeets', + 'paki', + 'pakis', + 'pickaninnie', + 'pickaninnies', + 'pickaninny', + 'pickaninnys', + 'raghead', + 'ragheads', + 'retard', + 'retards', + 'sambo', + 'sambos', + 'shemale', + 'shemales', + 'spade', + 'spades', + 'sperg', + 'spergs', + 'spic', + 'spics', + 'squaw', + 'squaws', + 'tard', + 'tards', + 'wetback', + 'wetbacks', + 'wigger', + 'wiggers', + 'wop', + 'wops', + 'yid', + 'yids', +] + export const reservedSubdomains: Record = [ ...atpSpecific, ...commonlyReserved, ...famousAccounts, + ...slurs, ].reduce((acc, cur) => { return { ...acc, From 0bac4e775242b1916ed4216ab113b9553facf8fa Mon Sep 17 00:00:00 2001 From: bnewbold Date: Wed, 12 Jul 2023 22:37:00 -0700 Subject: [PATCH 036/237] identifier: tweaks and additions to slur list (#1319) --- packages/identifier/src/reserved.ts | 54 ++++++++++++++--------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/identifier/src/reserved.ts b/packages/identifier/src/reserved.ts index c924831a966..f025b619571 100644 --- a/packages/identifier/src/reserved.ts +++ b/packages/identifier/src/reserved.ts @@ -1047,16 +1047,16 @@ const famousAccounts = [ 'zerohora', ] -// Needs additional work to add more words that should be caught and regex added to catch common variations +// naive, incomplete list of slurs and unacceptable words const slurs = [ - 'chinaman', - 'chinamen', + 'bluegum', 'chink', 'chinks', 'coolie', 'coolies', 'coon', 'coons', + 'coont', 'golliwog', 'golliwogs', 'gook', @@ -1064,13 +1064,12 @@ const slurs = [ 'gyp', 'gyps', 'half-breed', - 'half-breeds', 'halfbreed', + 'half-breeds', 'halfbreeds', 'heeb', 'heebs', - 'jap', - 'japs', + 'hitler', 'kaffer', 'kaffers', 'kaffir', @@ -1081,53 +1080,54 @@ const slurs = [ 'kafirs', 'kike', 'kikes', - 'kraut', - 'krauts', + 'kkk', + 'klukluxklan', + 'muzzie', + 'n1gga', + 'n1gger', + 'nazi ', + 'negorid', 'negress', - 'negro', - 'negros', 'nig', - 'nigs', - 'nig-nog', - 'nig-nogs', + 'nigg3r', + 'nigg4h', 'nigga', + 'niggah', 'niggas', + 'niggaz', 'nigger', + 'niggerachi', + 'niggerette', + 'niggerican', + 'niggerino', + 'niggeroid', 'niggers', + 'nigglet', 'nigguh', 'nigguhs', - 'pajeet', - 'pajeets', + 'nig-nog', + 'nig-nogs', + 'nigs', 'paki', 'pakis', + 'pedophile', 'pickaninnie', 'pickaninnies', 'pickaninny', 'pickaninnys', 'raghead', 'ragheads', - 'retard', - 'retards', + 'redskin', 'sambo', 'sambos', - 'shemale', - 'shemales', 'spade', 'spades', - 'sperg', - 'spergs', 'spic', 'spics', 'squaw', 'squaws', - 'tard', - 'tards', 'wetback', 'wetbacks', - 'wigger', - 'wiggers', - 'wop', - 'wops', 'yid', 'yids', ] From 89f21e9571a2bd356cec1d8f92cf574b1d16f3dd Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 13 Jul 2023 13:35:29 -0500 Subject: [PATCH 037/237] Refactor appview repo subscription for memleak (#1308) * refactor to remove closure in loop * move consecutive item out of p-queue --- packages/bsky/src/subscription/repo.ts | 90 ++++++++++++++------------ 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/packages/bsky/src/subscription/repo.ts b/packages/bsky/src/subscription/repo.ts index ca6385505b5..8a0a13ed135 100644 --- a/packages/bsky/src/subscription/repo.ts +++ b/packages/bsky/src/subscription/repo.ts @@ -19,7 +19,12 @@ import AppContext from '../context' import { Leader } from '../db/leader' import { IndexingService } from '../services/indexing' import { subLogger } from '../logger' -import { ConsecutiveList, LatestQueue, PartitionedQueue } from './util' +import { + ConsecutiveItem, + ConsecutiveList, + LatestQueue, + PartitionedQueue, +} from './util' const METHOD = ids.ComAtprotoSyncSubscribeRepos export const REPO_SUB_ID = 1000 @@ -63,33 +68,9 @@ export class RepoSubscription { } this.lastSeq = details.seq const item = this.consecutive.push(details.seq) - this.repoQueue - .add(details.repo, () => this.handleMessage(details.message)) - .catch((err) => { - // We log messages we can't process and move on. Barring a - // durable queue this is the best we can do for now: otherwise - // the cursor would get stuck on a poison message. - subLogger.error( - { - err, - provider: this.service, - message: loggableMessage(msg), - }, - 'repo subscription message processing error', - ) - }) - .finally(() => { - const latest = item.complete().at(-1) - if (!latest) return - this.cursorQueue - .add(() => this.handleCursor(latest)) - .catch((err) => { - subLogger.error( - { err, provider: this.service }, - 'repo subscription cursor error', - ) - }) - }) + this.repoQueue.add(details.repo, () => + this.handleMessage(item, details.message), + ) await this.repoQueue.main.onEmpty() // backpressure } }) @@ -123,18 +104,47 @@ export class RepoSubscription { await this.run() } - private async handleMessage(msg: ProcessableMessage) { - if (message.isCommit(msg)) { - await this.handleCommit(msg) - } else if (message.isHandle(msg)) { - await this.handleUpdateHandle(msg) - } else if (message.isTombstone(msg)) { - await this.handleTombstone(msg) - } else if (message.isMigrate(msg)) { - // Ignore migrations - } else { - const exhaustiveCheck: never = msg - throw new Error(`Unhandled message type: ${exhaustiveCheck['$type']}`) + private async handleMessage( + item: ConsecutiveItem, + msg: ProcessableMessage, + ) { + try { + if (message.isCommit(msg)) { + await this.handleCommit(msg) + } else if (message.isHandle(msg)) { + await this.handleUpdateHandle(msg) + } else if (message.isTombstone(msg)) { + await this.handleTombstone(msg) + } else if (message.isMigrate(msg)) { + // Ignore migrations + } else { + const exhaustiveCheck: never = msg + throw new Error(`Unhandled message type: ${exhaustiveCheck['$type']}`) + } + } catch (err) { + // We log messages we can't process and move on. Barring a + // durable queue this is the best we can do for now: otherwise + // the cursor would get stuck on a poison message. + subLogger.error( + { + err, + provider: this.service, + message: loggableMessage(msg), + }, + 'repo subscription message processing error', + ) + } finally { + const latest = item.complete().at(-1) + if (latest) { + this.cursorQueue + .add(() => this.handleCursor(latest)) + .catch((err) => { + subLogger.error( + { err, provider: this.service }, + 'repo subscription cursor error', + ) + }) + } } } From 6b51ecbbb2ce1eabb02749e83aea04b37a086ad9 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 13 Jul 2023 22:29:01 -0500 Subject: [PATCH 038/237] Handle validation improvements (#1336) * Handle matches and false positives for unacceptable words in handles * move handle validation logic to pds * missed merge * add cfg flag * encode lists * fix build issues * move words to cfg * tidy --------- Co-authored-by: Jaz Volpert --- packages/identifier/src/handle.ts | 58 +--------- packages/identifier/src/index.ts | 1 - packages/identifier/tests/handle.test.ts | 26 +---- .../com/atproto/admin/updateAccountHandle.ts | 42 ++----- .../api/com/atproto/identity/updateHandle.ts | 45 ++------ .../api/com/atproto/server/createAccount.ts | 41 ++----- packages/pds/src/config.ts | 19 ++++ packages/pds/src/handle/index.ts | 95 ++++++++++++++++ packages/pds/src/handle/moderation/index.ts | 66 +++++++++++ .../pds/src/handle/moderation/validator.ts | 106 ++++++++++++++++++ .../src => pds/src/handle}/reserved.ts | 86 -------------- packages/pds/tests/handle-validation.test.ts | 50 +++++++++ packages/pds/tests/handles.test.ts | 14 --- 13 files changed, 372 insertions(+), 277 deletions(-) create mode 100644 packages/pds/src/handle/index.ts create mode 100644 packages/pds/src/handle/moderation/index.ts create mode 100644 packages/pds/src/handle/moderation/validator.ts rename packages/{identifier/src => pds/src/handle}/reserved.ts (92%) create mode 100644 packages/pds/tests/handle-validation.test.ts diff --git a/packages/identifier/src/handle.ts b/packages/identifier/src/handle.ts index 12a4793f785..c42dc8b79dc 100644 --- a/packages/identifier/src/handle.ts +++ b/packages/identifier/src/handle.ts @@ -1,12 +1,10 @@ -import { reservedSubdomains } from './reserved' - export const INVALID_HANDLE = 'handle.invalid' // Currently these are registration-time restrictions, not protocol-level // restrictions. We have a couple accounts in the wild that we need to clean up // before hard-disallow. // See also: https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains -const DISALLOWED_TLDS = [ +export const DISALLOWED_TLDS = [ '.local', '.arpa', '.invalid', @@ -106,60 +104,12 @@ export const isValidHandle = (handle: string): boolean => { } throw err } - return true -} -export const ensureHandleServiceConstraints = ( - handle: string, - availableUserDomains: string[], - reserved = reservedSubdomains, -): void => { - const disallowedTld = DISALLOWED_TLDS.find((domain) => - handle.endsWith(domain), - ) - if (disallowedTld) { - throw new DisallowedDomainError('Handle TLD is invalid or disallowed') - } - const supportedDomain = availableUserDomains.find((domain) => - handle.endsWith(domain), - ) - if (!supportedDomain) { - throw new UnsupportedDomainError('Not a supported handle domain') - } - const front = handle.slice(0, handle.length - supportedDomain.length) - if (front.indexOf('.') > -1) { - throw new InvalidHandleError('Invalid characters in handle') - } - if (front.length < 3) { - throw new InvalidHandleError('Handle too short') - } - if (handle.length > 30) { - throw new InvalidHandleError('Handle too long') - } - if (reserved[front]) { - throw new ReservedHandleError('Reserved handle') - } + return true } -export const fulfillsHandleServiceConstraints = ( - handle: string, - availableUserDomains: string[], - reserved = reservedSubdomains, -): boolean => { - try { - ensureHandleServiceConstraints(handle, availableUserDomains, reserved) - } catch (err) { - if ( - err instanceof InvalidHandleError || - err instanceof ReservedHandleError || - err instanceof UnsupportedDomainError || - err instanceof DisallowedDomainError - ) { - return false - } - throw err - } - return true +export const isValidTld = (handle: string): boolean => { + return !DISALLOWED_TLDS.some((domain) => handle.endsWith(domain)) } export class InvalidHandleError extends Error {} diff --git a/packages/identifier/src/index.ts b/packages/identifier/src/index.ts index 87f0b851b0d..4f92a9d5180 100644 --- a/packages/identifier/src/index.ts +++ b/packages/identifier/src/index.ts @@ -1,3 +1,2 @@ export * from './handle' export * from './did' -export * from './reserved' diff --git a/packages/identifier/tests/handle.test.ts b/packages/identifier/tests/handle.test.ts index 1ce36c7a3de..2fa56b4cfe2 100644 --- a/packages/identifier/tests/handle.test.ts +++ b/packages/identifier/tests/handle.test.ts @@ -1,6 +1,5 @@ import { ensureValidHandle, - ensureHandleServiceConstraints, normalizeAndEnsureValidHandle, ensureValidHandleRegex, InvalidHandleError, @@ -193,30 +192,7 @@ describe('handle validation', () => { }) }) -describe('service constraints & normalization', () => { - const domains = ['.bsky.app', '.test'] - it('throw on handles that violate service constraints', () => { - const expectThrow = (handle: string, err: string) => { - expect(() => ensureHandleServiceConstraints(handle, domains)).toThrow(err) - } - - expectThrow('john.bsky.io', 'Not a supported handle domain') - expectThrow('john.com', 'Not a supported handle domain') - expectThrow('j.test', 'Handle too short') - expectThrow('uk.test', 'Handle too short') - expectThrow('john.test.bsky.app', 'Invalid characters in handle') - expectThrow('about.test', 'Reserved handle') - expectThrow('atp.test', 'Reserved handle') - expectThrow('barackobama.test', 'Reserved handle') - - expectThrow('atproto.local', 'Handle TLD is invalid or disallowed') - expectThrow('atproto.arpa', 'Handle TLD is invalid or disallowed') - expectThrow('atproto.invalid', 'Handle TLD is invalid or disallowed') - expectThrow('atproto.localhost', 'Handle TLD is invalid or disallowed') - expectThrow('atproto.onion', 'Handle TLD is invalid or disallowed') - expectThrow('atproto.internal', 'Handle TLD is invalid or disallowed') - }) - +describe('normalization', () => { it('normalizes handles', () => { const normalized = normalizeAndEnsureValidHandle('JoHn.TeST') expect(normalized).toBe('john.test') diff --git a/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts b/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts index 2d9346c2c1d..e394a9f3dd3 100644 --- a/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts +++ b/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts @@ -1,5 +1,5 @@ import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' -import * as ident from '@atproto/identifier' +import { normalizeAndValidateHandle } from '../../../../handle' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { @@ -11,42 +11,18 @@ import { httpLogger } from '../../../../logger' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.updateAccountHandle({ auth: ctx.roleVerifier, - handler: async ({ input, req, auth }) => { + handler: async ({ input, auth }) => { if (!auth.credentials.admin) { throw new AuthRequiredError('Insufficient privileges') } const { did } = input.body - let handle: string - try { - handle = ident.normalizeAndEnsureValidHandle(input.body.handle) - } catch (err) { - if (err instanceof ident.InvalidHandleError) { - throw new InvalidRequestError(err.message, 'InvalidHandle') - } else { - throw err - } - } - try { - ident.ensureHandleServiceConstraints( - handle, - ctx.cfg.availableUserDomains, - ) - } catch (err) { - if (err instanceof ident.UnsupportedDomainError) { - throw new InvalidRequestError( - 'Unsupported domain', - 'UnsupportedDomain', - ) - } else if (err instanceof ident.ReservedHandleError) { - // we allow this - req.log.info( - { did, handle: input.body }, - 'admin setting reserved handle', - ) - } else { - throw err - } - } + const handle = await normalizeAndValidateHandle({ + ctx, + handle: input.body.handle, + did, + allowReserved: true, + }) + const existingAccnt = await ctx.services.account(ctx.db).getAccount(did) if (!existingAccnt) { throw new InvalidRequestError(`Account not found: ${did}`) diff --git a/packages/pds/src/api/com/atproto/identity/updateHandle.ts b/packages/pds/src/api/com/atproto/identity/updateHandle.ts index 0119ae79810..356165e72f7 100644 --- a/packages/pds/src/api/com/atproto/identity/updateHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/updateHandle.ts @@ -1,5 +1,5 @@ import { InvalidRequestError } from '@atproto/xrpc-server' -import * as ident from '@atproto/identifier' +import { normalizeAndValidateHandle } from '../../../../handle' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { @@ -7,45 +7,18 @@ import { UserAlreadyExistsError, } from '../../../../services/account' import { httpLogger } from '../../../../logger' +import { backgroundHandleCheckForFlag } from '../../../../handle/moderation' export default function (server: Server, ctx: AppContext) { server.com.atproto.identity.updateHandle({ auth: ctx.accessVerifierCheckTakedown, handler: async ({ auth, input }) => { const requester = auth.credentials.did - let handle: string - try { - handle = ident.normalizeAndEnsureValidHandle(input.body.handle) - } catch (err) { - if (err instanceof ident.InvalidHandleError) { - throw new InvalidRequestError(err.message, 'InvalidHandle') - } - throw err - } - - // test against our service constraints - // if not a supported domain, then we must check that the domain correctly links to the DID - try { - ident.ensureHandleServiceConstraints( - handle, - ctx.cfg.availableUserDomains, - ) - } catch (err) { - if (err instanceof ident.UnsupportedDomainError) { - const did = await ctx.idResolver.handle.resolve(handle) - if (did !== requester) { - throw new InvalidRequestError( - 'External handle did not resolve to DID', - ) - } - } else if (err instanceof ident.InvalidHandleError) { - throw new InvalidRequestError(err.message, 'InvalidHandle') - } else if (err instanceof ident.ReservedHandleError) { - throw new InvalidRequestError(err.message, 'HandleNotAvailable') - } else { - throw err - } - } + const handle = await normalizeAndValidateHandle({ + ctx, + handle: input.body.handle, + did: requester, + }) // Pessimistic check to handle spam: also enforced by updateHandle() and the db. const available = await ctx.services @@ -81,6 +54,10 @@ export default function (server: Server, ctx: AppContext) { 'failed to sequence handle update', ) } + + if (ctx.cfg.unacceptableHandleWordsB64) { + backgroundHandleCheckForFlag({ ctx, handle, did: requester }) + } }, }) } diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 62391034aa3..2b0e5f72224 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -1,5 +1,5 @@ import { InvalidRequestError } from '@atproto/xrpc-server' -import * as ident from '@atproto/identifier' +import { normalizeAndValidateHandle } from '../../../../handle' import * as plc from '@did-plc/lib' import * as scrypt from '../../../../db/scrypt' import { Server } from '../../../../lexicon' @@ -9,6 +9,7 @@ import { UserAlreadyExistsError } from '../../../../services/account' import AppContext from '../../../../context' import Database from '../../../../db' import { AtprotoData } from '@atproto/identity' +import { backgroundHandleCheckForFlag } from '../../../../handle/moderation' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.createAccount(async ({ input, req }) => { @@ -22,7 +23,11 @@ export default function (server: Server, ctx: AppContext) { } // normalize & ensure valid handle - const handle = await ensureValidHandle(ctx, input.body) + const handle = await normalizeAndValidateHandle({ + ctx, + handle: input.body.handle, + did: input.body.did, + }) // check that the invite code still has uses if (ctx.cfg.inviteRequired && inviteCode) { @@ -101,6 +106,10 @@ export default function (server: Server, ctx: AppContext) { } }) + if (ctx.cfg.unacceptableHandleWordsB64) { + backgroundHandleCheckForFlag({ ctx, handle, did: result.did }) + } + return { encoding: 'application/json', body: { @@ -146,34 +155,6 @@ export const ensureCodeIsAvailable = async ( } } -const ensureValidHandle = async ( - ctx: AppContext, - input: CreateAccountInput, -): Promise => { - try { - const handle = ident.normalizeAndEnsureValidHandle(input.handle) - ident.ensureHandleServiceConstraints(handle, ctx.cfg.availableUserDomains) - return handle - } catch (err) { - if (err instanceof ident.InvalidHandleError) { - throw new InvalidRequestError(err.message, 'InvalidHandle') - } else if (err instanceof ident.ReservedHandleError) { - throw new InvalidRequestError(err.message, 'HandleNotAvailable') - } else if (err instanceof ident.UnsupportedDomainError) { - if (input.did === undefined) { - throw new InvalidRequestError(err.message, 'UnsupportedDomain') - } - const resolvedHandleDid = await ctx.idResolver.handle.resolve( - input.handle, - ) - if (input.did !== resolvedHandleDid) { - throw new InvalidRequestError('External handle did not resolve to DID') - } - } - throw err - } -} - const getDidAndPlcOp = async ( ctx: AppContext, handle: string, diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index 0807ae390bc..4c41c674e4f 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -50,6 +50,8 @@ export interface ServerConfigValues { hiveApiKey?: string labelerDid: string labelerKeywords: Record + unacceptableHandleWordsB64?: string + falsePositiveHandleWordsB64?: string feedGenDid?: string @@ -163,6 +165,13 @@ export class ServerConfig { const labelerDid = process.env.LABELER_DID || 'did:example:labeler' const labelerKeywords = {} + const unacceptableHandleWordsB64 = nonemptyString( + process.env.UNACCEPTABLE_HANDLE_WORDS_B64, + ) + const falsePositiveHandleWordsB64 = nonemptyString( + process.env.FALSE_POSITIVE_HANDLE_WORDS_B64, + ) + const feedGenDid = process.env.FEED_GEN_DID const dbPostgresUrl = process.env.DB_POSTGRES_URL @@ -234,6 +243,8 @@ export class ServerConfig { hiveApiKey, labelerDid, labelerKeywords, + unacceptableHandleWordsB64, + falsePositiveHandleWordsB64, feedGenDid, maxSubscriptionBuffer, repoBackfillLimitMs, @@ -425,6 +436,14 @@ export class ServerConfig { return this.cfg.labelerKeywords } + get unacceptableHandleWordsB64() { + return this.cfg.unacceptableHandleWordsB64 + } + + get falsePositiveHandleWordsB64() { + return this.cfg.falsePositiveHandleWordsB64 + } + get feedGenDid() { return this.cfg.feedGenDid } diff --git a/packages/pds/src/handle/index.ts b/packages/pds/src/handle/index.ts new file mode 100644 index 00000000000..edad6c736e5 --- /dev/null +++ b/packages/pds/src/handle/index.ts @@ -0,0 +1,95 @@ +import * as ident from '@atproto/identifier' +import { InvalidRequestError } from '@atproto/xrpc-server' +import { reservedSubdomains } from './reserved' +import { hasExplicitSlur } from './moderation' +import AppContext from '../context' + +export const normalizeAndValidateHandle = async (opts: { + ctx: AppContext + handle: string + did?: string + allowReserved?: boolean +}): Promise => { + const { ctx, did, allowReserved } = opts + // base formatting validation + const handle = baseNormalizeAndValidate(opts.handle) + // tld validation + if (!ident.isValidTld) { + throw new InvalidRequestError( + 'Handle TLD is invalid or disallowed', + 'InvalidHandle', + ) + } + // slur check etc + if (hasExplicitSlur(handle)) { + throw new InvalidRequestError( + 'Inappropriate language in handle', + 'InvalidHandle', + ) + } + if (isServiceDomain(handle, ctx.cfg.availableUserDomains)) { + // verify constraints on a service domain + ensureHandleServiceConstraints( + handle, + ctx.cfg.availableUserDomains, + allowReserved, + ) + } else { + if (opts.did === undefined) { + throw new InvalidRequestError( + 'Not a supported handle domain', + 'UnsupportedDomain', + ) + } + // verify resolution of a non-service domain + const resolvedDid = await ctx.idResolver.handle.resolve(handle) + if (resolvedDid !== did) { + throw new InvalidRequestError('External handle did not resolve to DID') + } + } + return handle +} + +export const baseNormalizeAndValidate = (handle: string) => { + try { + const normalized = ident.normalizeAndEnsureValidHandle(handle) + return normalized + } catch (err) { + if (err instanceof ident.InvalidHandleError) { + throw new InvalidRequestError(err.message, 'InvalidHandle') + } + throw err + } +} + +export const isServiceDomain = ( + handle: string, + availableUserDomains: string[], +): boolean => { + return availableUserDomains.some((domain) => handle.endsWith(domain)) +} + +export const ensureHandleServiceConstraints = ( + handle: string, + availableUserDomains: string[], + allowReserved = false, +): void => { + const supportedDomain = + availableUserDomains.find((domain) => handle.endsWith(domain)) ?? '' + const front = handle.slice(0, handle.length - supportedDomain.length) + if (front.includes('.')) { + throw new InvalidRequestError( + 'Invalid characters in handle', + 'InvalidHandle', + ) + } + if (front.length < 3) { + throw new InvalidRequestError('Handle too short', 'InvalidHandle') + } + if (handle.length > 30) { + throw new InvalidRequestError('Handle too long', 'InvalidHandle') + } + if (!allowReserved && reservedSubdomains[front]) { + throw new InvalidRequestError('Reserved handle', 'HandleNotAvailable') + } +} diff --git a/packages/pds/src/handle/moderation/index.ts b/packages/pds/src/handle/moderation/index.ts new file mode 100644 index 00000000000..3bbf013e8c6 --- /dev/null +++ b/packages/pds/src/handle/moderation/index.ts @@ -0,0 +1,66 @@ +import * as ui8 from 'uint8arrays' +import AppContext from '../../context' +import { REASONOTHER } from '../../lexicon/types/com/atproto/moderation/defs' +import { UnacceptableHandleValidator } from './validator' + +// regexes taken from: https://github.com/Blank-Cheque/Slurs +/* eslint-disable no-misleading-character-class */ +const explicitSlurRegexes = [ + /\b[cĆćĈĉČčĊċÇçḈḉȻȼꞒꞓꟄꞔƇƈɕ][hĤĥȞȟḦḧḢḣḨḩḤḥḪḫH̱ẖĦħⱧⱨꞪɦꞕΗНн][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn][kḰḱǨǩĶķḲḳḴḵƘƙⱩⱪᶄꝀꝁꝂꝃꝄꝅꞢꞣ][sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/, + /\b[cĆćĈĉČčĊċÇçḈḉȻȼꞒꞓꟄꞔƇƈɕ][ÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOo0]{2}[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn][sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/, + /[fḞḟƑƒꞘꞙᵮᶂ][aÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa@4][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶGgqꝖꝗꝘꝙɋʠ]{1,2}([ÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOo0e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEeiÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][tŤťṪṫŢţṬṭȚțṰṱṮṯŦŧȾⱦƬƭƮʈT̈ẗᵵƫȶ]{1,2}([rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][yÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ]|[rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe])?)?[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/, + /\b[kḰḱǨǩĶķḲḳḴḵƘƙⱩⱪᶄꝀꝁꝂꝃꝄꝅꞢꞣ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLlyÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ][kḰḱǨǩĶķḲḳḴḵƘƙⱩⱪᶄꝀꝁꝂꝃꝄꝅꞢꞣ][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe]([rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][yÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ]|[rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe])?[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]*\b/, + /\b([sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ][a4ÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa][nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn][dĎďḊḋḐḑD̦d̦ḌḍḒḓḎḏĐđÐðƉɖƊɗᵭᶁᶑȡ])?[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLloÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOoІіa4ÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶGgqꝖꝗꝘꝙɋʠ]{1,2}(l[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe]t|[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEeaÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ]?|n[ÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOo0][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶGgqꝖꝗꝘꝙɋʠ]|[a4ÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa]?)?[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/, + /\b[tŤťṪṫŢţṬṭȚțṰṱṮṯŦŧȾⱦƬƭƮʈT̈ẗᵵƫȶ][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][aÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa4]+[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn]{1,2}([iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe]|[yÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ]|[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ])[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/, +] + +export const hasExplicitSlur = (handle: string): boolean => { + return explicitSlurRegexes.some((reg) => reg.test(handle)) +} + +const decode = (encoded: string): string[] => { + return ui8.toString(ui8.fromString(encoded, 'base64'), 'utf8').split(',') +} + +let _validator: UnacceptableHandleValidator | undefined = undefined +const getValidator = ( + unacceptable: string, + falsePositives?: string, +): UnacceptableHandleValidator => { + if (!_validator) { + _validator = new UnacceptableHandleValidator( + decode(unacceptable), + falsePositives ? decode(falsePositives) : undefined, + ) + } + return _validator +} + +export const backgroundHandleCheckForFlag = (opts: { + ctx: AppContext + handle: string + did: string +}) => { + const { ctx, handle, did } = opts + if (!ctx.cfg.unacceptableHandleWordsB64) { + return + } + const validator = getValidator( + ctx.cfg.unacceptableHandleWordsB64, + ctx.cfg.falsePositiveHandleWordsB64, + ) + const possibleSlurs = validator.getMatches(handle) + if (possibleSlurs.length < 1) { + return + } + ctx.backgroundQueue.add(async () => { + await ctx.services.moderation(ctx.db).report({ + reasonType: REASONOTHER, + reason: `Automatically flagged for possible slurs: ${possibleSlurs.join( + ', ', + )}`, + subject: { did }, + reportedBy: ctx.cfg.serverDid, + }) + }) +} diff --git a/packages/pds/src/handle/moderation/validator.ts b/packages/pds/src/handle/moderation/validator.ts new file mode 100644 index 00000000000..d832e828334 --- /dev/null +++ b/packages/pds/src/handle/moderation/validator.ts @@ -0,0 +1,106 @@ +import { dedupeStrs } from '@atproto/common' + +export class UnacceptableHandleValidator { + private bannedWords: Set + private falsePositives: Set + + constructor(bannedWords: string[], falsePositives: string[] = []) { + this.bannedWords = new Set(bannedWords.map((word) => word.toLowerCase())) + this.falsePositives = new Set( + falsePositives.map((word) => word.toLowerCase()), + ) + } + + private normalize(domain: string): string[] { + const withoutSymbols = domain.replace(/[\W_]+/g, '') // Remove non-alphanumeric characters + const lowercase = withoutSymbols.toLowerCase() + + // Replace common leetspeak characters + const leetSpeakReplacements: { [key: string]: string[] } = { + '0': ['o'], + '8': ['b'], + '3': ['e'], + '4': ['a'], + '6': ['g'], + '1': ['i', 'l'], + '5': ['s'], + '7': ['t'], + } + + return this.generatePermutations(lowercase, leetSpeakReplacements) + } + + private generatePermutations( + domain: string, + leetSpeakReplacements: { [key: string]: string[] }, + ): string[] { + const results: string[] = [] + + const leetChars = Object.keys(leetSpeakReplacements) + const firstLeetCharIndex = [...domain].findIndex((char) => + leetChars.includes(char), + ) + + if (firstLeetCharIndex === -1) { + // No leetspeak characters left in the string + results.push(domain) + } else { + const char = domain[firstLeetCharIndex] + const beforeChar = domain.slice(0, firstLeetCharIndex) + const afterChar = domain.slice(firstLeetCharIndex + 1) + + // For each replacement, generate all possible combinations + for (const replacement of leetSpeakReplacements[char]) { + const replaced = beforeChar + replacement + afterChar + + // Recursively generate all permutations for the rest of the string + const otherPermutations = this.generatePermutations( + replaced, + leetSpeakReplacements, + ) + + // Add these permutations to the results + results.push(...otherPermutations) + } + } + + return dedupeStrs(results) + } + + public getMatches(domain: string): string[] { + const normalizedDomains = this.normalize(domain) + + const foundUnacceptableWords: string[] = [] + + for (const normalizedDomain of normalizedDomains) { + for (const word of this.bannedWords) { + const match = normalizedDomain.indexOf(word) + if (match > -1) { + let isFalsePositive = false + for (const falsePositive of this.falsePositives) { + const s_fp = falsePositive.indexOf(word) + const s_nd = match - s_fp + const wordToMatch = normalizedDomain.slice( + s_nd, + s_nd + falsePositive.length, + ) + if (wordToMatch === falsePositive) { + isFalsePositive = true + break + } + } + + if (!isFalsePositive) { + foundUnacceptableWords.push(word) + } + } + } + } + + if (foundUnacceptableWords.length > 0) { + return foundUnacceptableWords + } + + return [] + } +} diff --git a/packages/identifier/src/reserved.ts b/packages/pds/src/handle/reserved.ts similarity index 92% rename from packages/identifier/src/reserved.ts rename to packages/pds/src/handle/reserved.ts index f025b619571..c49c85f5378 100644 --- a/packages/identifier/src/reserved.ts +++ b/packages/pds/src/handle/reserved.ts @@ -1047,96 +1047,10 @@ const famousAccounts = [ 'zerohora', ] -// naive, incomplete list of slurs and unacceptable words -const slurs = [ - 'bluegum', - 'chink', - 'chinks', - 'coolie', - 'coolies', - 'coon', - 'coons', - 'coont', - 'golliwog', - 'golliwogs', - 'gook', - 'gooks', - 'gyp', - 'gyps', - 'half-breed', - 'halfbreed', - 'half-breeds', - 'halfbreeds', - 'heeb', - 'heebs', - 'hitler', - 'kaffer', - 'kaffers', - 'kaffir', - 'kaffirs', - 'kaffre', - 'kaffres', - 'kafir', - 'kafirs', - 'kike', - 'kikes', - 'kkk', - 'klukluxklan', - 'muzzie', - 'n1gga', - 'n1gger', - 'nazi ', - 'negorid', - 'negress', - 'nig', - 'nigg3r', - 'nigg4h', - 'nigga', - 'niggah', - 'niggas', - 'niggaz', - 'nigger', - 'niggerachi', - 'niggerette', - 'niggerican', - 'niggerino', - 'niggeroid', - 'niggers', - 'nigglet', - 'nigguh', - 'nigguhs', - 'nig-nog', - 'nig-nogs', - 'nigs', - 'paki', - 'pakis', - 'pedophile', - 'pickaninnie', - 'pickaninnies', - 'pickaninny', - 'pickaninnys', - 'raghead', - 'ragheads', - 'redskin', - 'sambo', - 'sambos', - 'spade', - 'spades', - 'spic', - 'spics', - 'squaw', - 'squaws', - 'wetback', - 'wetbacks', - 'yid', - 'yids', -] - export const reservedSubdomains: Record = [ ...atpSpecific, ...commonlyReserved, ...famousAccounts, - ...slurs, ].reduce((acc, cur) => { return { ...acc, diff --git a/packages/pds/tests/handle-validation.test.ts b/packages/pds/tests/handle-validation.test.ts new file mode 100644 index 00000000000..c0174fb533a --- /dev/null +++ b/packages/pds/tests/handle-validation.test.ts @@ -0,0 +1,50 @@ +import { isValidTld } from '@atproto/identifier' +import { ensureHandleServiceConstraints } from '../src/handle' +import { UnacceptableHandleValidator } from '../src/handle/moderation/validator' + +describe('handle validation', () => { + it('validates service constraints', () => { + const domains = ['.bsky.app', '.test'] + const expectThrow = (handle: string, err: string) => { + expect(() => ensureHandleServiceConstraints(handle, domains)).toThrow(err) + } + expectThrow('john.bsky.io', 'Invalid characters in handle') + expectThrow('john.com', 'Invalid characters in handle') + expectThrow('j.test', 'Handle too short') + expectThrow('uk.test', 'Handle too short') + expectThrow('john.test.bsky.app', 'Invalid characters in handle') + expectThrow('about.test', 'Reserved handle') + expectThrow('atp.test', 'Reserved handle') + expectThrow('barackobama.test', 'Reserved handle') + }) + + it('handles bad tlds', () => { + expect(isValidTld('atproto.local')).toBe(false) + expect(isValidTld('atproto.arpa')).toBe(false) + expect(isValidTld('atproto.invalid')).toBe(false) + expect(isValidTld('atproto.localhost')).toBe(false) + expect(isValidTld('atproto.onion')).toBe(false) + expect(isValidTld('atproto.internal')).toBe(false) + }) + + const validator = new UnacceptableHandleValidator( + ['evil', 'mean', 'bad'], + ['baddie'], + ) + + it('identifies offensive handles', () => { + expect(validator.getMatches('evil.john.test')).toMatchObject(['evil']) + expect(validator.getMatches('john.evil.test')).toMatchObject(['evil']) + expect(validator.getMatches('john.test.evil')).toMatchObject(['evil']) + expect(validator.getMatches('ev1l.test.john')).toMatchObject(['evil']) + expect(validator.getMatches('ev-1l.test.john')).toMatchObject(['evil']) + expect(validator.getMatches('ev-11.test.john')).toMatchObject(['evil']) + expect(validator.getMatches('ev.-1.l-test.john')).toMatchObject(['evil']) + }) + + it('identifies non-offensive handles', () => { + expect(validator.getMatches('john.test')).toHaveLength(0) + expect(validator.getMatches('good.john.test')).toHaveLength(0) + expect(validator.getMatches('john.baddie.test')).toHaveLength(0) + }) +}) diff --git a/packages/pds/tests/handles.test.ts b/packages/pds/tests/handles.test.ts index 631ff55fa24..fdcd4a08516 100644 --- a/packages/pds/tests/handles.test.ts +++ b/packages/pds/tests/handles.test.ts @@ -263,20 +263,6 @@ describe('handles', () => { expect(profile.data.handle).toBe('dril.test') }) - it('disallows setting handle to an off-service domain', async () => { - const attempt = agent.api.com.atproto.admin.updateAccountHandle( - { - did: bob, - handle: 'bob.external', - }, - { - headers: { authorization: util.adminAuth() }, - encoding: 'application/json', - }, - ) - await expect(attempt).rejects.toThrow('Unsupported domain') - }) - it('requires admin auth', async () => { const attempt = agent.api.com.atproto.admin.updateAccountHandle( { From 53c49561468e73fe45737faf6391ce24f3fdc58f Mon Sep 17 00:00:00 2001 From: devin ivy Date: Fri, 14 Jul 2023 14:37:32 -0700 Subject: [PATCH 039/237] Allow moderators to take and reverse actor takedowns (#1330) allow moderators to take and reverse actor takedowns --- .../com/atproto/admin/reverseModerationAction.ts | 6 +++--- .../com/atproto/admin/takeModerationAction.ts | 4 ++-- packages/bsky/tests/moderation.test.ts | 16 +++++++++------- .../com/atproto/admin/reverseModerationAction.ts | 4 ++-- .../com/atproto/admin/takeModerationAction.ts | 4 ++-- packages/pds/tests/moderation.test.ts | 16 +++++++++------- 6 files changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts b/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts index 0a3e1126ba8..2fe61649d27 100644 --- a/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts +++ b/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts @@ -43,14 +43,14 @@ export default function (server: Server, ctx: AppContext) { 'Must be a full moderator to reverse this type of action', ) } - // if less than admin access then can reverse takedown on an account + // if less than moderator access then cannot reverse takedown on an account if ( - !access.admin && + !access.moderator && existing.action === TAKEDOWN && existing.subjectType === 'com.atproto.admin.defs#repoRef' ) { throw new AuthRequiredError( - 'Must be an admin to reverse an account takedown', + 'Must be a full moderator to reverse an account takedown', ) } diff --git a/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts b/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts index 42e1b7034d7..d045475dcbb 100644 --- a/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts +++ b/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts @@ -30,9 +30,9 @@ export default function (server: Server, ctx: AppContext) { // apply access rules // if less than admin access then can not takedown an account - if (!access.admin && action === TAKEDOWN && 'did' in subject) { + if (!access.moderator && action === TAKEDOWN && 'did' in subject) { throw new AuthRequiredError( - 'Must be an admin to perform an account takedown', + 'Must be a full moderator to perform an account takedown', ) } // if less than moderator access then can only take ack and escalation actions diff --git a/packages/bsky/tests/moderation.test.ts b/packages/bsky/tests/moderation.test.ts index 08067ef61f7..946b2d29a30 100644 --- a/packages/bsky/tests/moderation.test.ts +++ b/packages/bsky/tests/moderation.test.ts @@ -961,9 +961,9 @@ describe('moderation', () => { ) }) - it('does not allow non-admin moderators to takedown.', async () => { - const attemptTakedownMod = - agent.api.com.atproto.admin.takeModerationAction( + it('allows full moderators to takedown.', async () => { + const { data: action } = + await agent.api.com.atproto.admin.takeModerationAction( { action: TAKEDOWN, createdBy: 'did:example:moderator', @@ -978,9 +978,11 @@ describe('moderation', () => { headers: network.bsky.adminAuthHeaders('moderator'), }, ) - await expect(attemptTakedownMod).rejects.toThrow( - 'Must be an admin to perform an account takedown', - ) + // cleanup + await reverse(action.id) + }) + + it('does not allow non-full moderators to takedown.', async () => { const attemptTakedownTriage = agent.api.com.atproto.admin.takeModerationAction( { @@ -998,7 +1000,7 @@ describe('moderation', () => { }, ) await expect(attemptTakedownTriage).rejects.toThrow( - 'Must be an admin to perform an account takedown', + 'Must be a full moderator to perform an account takedown', ) }) diff --git a/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts b/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts index 2d7bf94673e..05991968d02 100644 --- a/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts @@ -43,9 +43,9 @@ export default function (server: Server, ctx: AppContext) { 'Must be a full moderator to reverse this type of action', ) } - // if less than admin access then can reverse takedown on an account + // if less than moderator access then cannot reverse takedown on an account if ( - !access.admin && + !access.moderator && existing.action === TAKEDOWN && existing.subjectType === 'com.atproto.admin.defs#repoRef' ) { diff --git a/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts b/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts index d4d0639c0f4..1a8feedd98b 100644 --- a/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts @@ -30,9 +30,9 @@ export default function (server: Server, ctx: AppContext) { // apply access rules // if less than admin access then can not takedown an account - if (!access.admin && action === TAKEDOWN && 'did' in subject) { + if (!access.moderator && action === TAKEDOWN && 'did' in subject) { throw new AuthRequiredError( - 'Must be an admin to perform an account takedown', + 'Must be a full moderator to perform an account takedown', ) } // if less than moderator access then can only take ack and escalation actions diff --git a/packages/pds/tests/moderation.test.ts b/packages/pds/tests/moderation.test.ts index 17c3ea0d92a..48743809815 100644 --- a/packages/pds/tests/moderation.test.ts +++ b/packages/pds/tests/moderation.test.ts @@ -984,9 +984,9 @@ describe('moderation', () => { ) }) - it('does not allow non-admin moderators to takedown.', async () => { - const attemptTakedownMod = - agent.api.com.atproto.admin.takeModerationAction( + it('allows full moderators to takedown.', async () => { + const { data: action } = + await agent.api.com.atproto.admin.takeModerationAction( { action: TAKEDOWN, createdBy: 'did:example:moderator', @@ -1001,9 +1001,11 @@ describe('moderation', () => { headers: { authorization: moderatorAuth() }, }, ) - await expect(attemptTakedownMod).rejects.toThrow( - 'Must be an admin to perform an account takedown', - ) + // cleanup + await reverse(action.id) + }) + + it('does not allow non-full moderators to takedown.', async () => { const attemptTakedownTriage = agent.api.com.atproto.admin.takeModerationAction( { @@ -1021,7 +1023,7 @@ describe('moderation', () => { }, ) await expect(attemptTakedownTriage).rejects.toThrow( - 'Must be an admin to perform an account takedown', + 'Must be a full moderator to perform an account takedown', ) }) From b9ca76f01221532e80fa71550bddf8d99e31ca12 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Sat, 15 Jul 2023 01:05:56 +0200 Subject: [PATCH 040/237] :sparkles: Allow searching reports by moderator did (#1283) * :sparkles: Allow searching reports by moderator did * :white_check_mark: Remove .only flags on tests * :white_check_mark: Update snapshot * :white_check_mark: Add checks for did match in actions --- .../atproto/admin/getModerationReports.json | 5 +++ packages/api/src/client/lexicons.ts | 8 +++- .../com/atproto/admin/getModerationReports.ts | 2 + .../com/atproto/admin/getModerationReports.ts | 2 + packages/bsky/src/lexicon/lexicons.ts | 8 +++- .../com/atproto/admin/getModerationReports.ts | 2 + .../bsky/src/services/moderation/index.ts | 26 +++++++++--- .../com/atproto/admin/getModerationReports.ts | 2 + packages/pds/src/lexicon/lexicons.ts | 8 +++- .../com/atproto/admin/getModerationReports.ts | 2 + packages/pds/src/services/moderation/index.ts | 26 +++++++++--- .../get-moderation-reports.test.ts.snap | 40 +++++++++++++++++++ .../admin/get-moderation-reports.test.ts | 35 ++++++++++++++++ 13 files changed, 151 insertions(+), 15 deletions(-) diff --git a/lexicons/com/atproto/admin/getModerationReports.json b/lexicons/com/atproto/admin/getModerationReports.json index a7f7bf83c4a..ad930389147 100644 --- a/lexicons/com/atproto/admin/getModerationReports.json +++ b/lexicons/com/atproto/admin/getModerationReports.json @@ -10,6 +10,11 @@ "properties": { "subject": { "type": "string" }, "ignoreSubjects": { "type": "array", "items": { "type": "string" } }, + "actionedBy": { + "type": "string", + "format": "did", + "description": "Get all reports that were actioned by a specific moderator" + }, "reporters": { "type": "array", "items": { "type": "string" }, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 68186fdb49e..3039bef31f8 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -865,6 +865,12 @@ export const schemaDict = { type: 'string', }, }, + actionedBy: { + type: 'string', + format: 'did', + description: + 'Get all reports that were actioned by a specific moderator', + }, reporters: { type: 'array', items: { @@ -6327,7 +6333,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'A skeleton of a timeline', + description: 'A skeleton of a timeline - UNSPECCED & WILL GO AWAY SOON', parameters: { type: 'params', properties: { diff --git a/packages/api/src/client/types/com/atproto/admin/getModerationReports.ts b/packages/api/src/client/types/com/atproto/admin/getModerationReports.ts index 6897a374e38..cc6c6f00f3c 100644 --- a/packages/api/src/client/types/com/atproto/admin/getModerationReports.ts +++ b/packages/api/src/client/types/com/atproto/admin/getModerationReports.ts @@ -11,6 +11,8 @@ import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { subject?: string ignoreSubjects?: string[] + /** Get all reports that were actioned by a specific moderator */ + actionedBy?: string /** Filter reports made by one or more DIDs */ reporters?: string[] resolved?: boolean diff --git a/packages/bsky/src/api/com/atproto/admin/getModerationReports.ts b/packages/bsky/src/api/com/atproto/admin/getModerationReports.ts index 5eb74b7e022..85c53f78758 100644 --- a/packages/bsky/src/api/com/atproto/admin/getModerationReports.ts +++ b/packages/bsky/src/api/com/atproto/admin/getModerationReports.ts @@ -15,6 +15,7 @@ export default function (server: Server, ctx: AppContext) { ignoreSubjects, reverse = false, reporters = [], + actionedBy, } = params const moderationService = services.moderation(db) const results = await moderationService.getReports({ @@ -26,6 +27,7 @@ export default function (server: Server, ctx: AppContext) { ignoreSubjects, reverse, reporters, + actionedBy, }) return { encoding: 'application/json', diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 68186fdb49e..3039bef31f8 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -865,6 +865,12 @@ export const schemaDict = { type: 'string', }, }, + actionedBy: { + type: 'string', + format: 'did', + description: + 'Get all reports that were actioned by a specific moderator', + }, reporters: { type: 'array', items: { @@ -6327,7 +6333,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'A skeleton of a timeline', + description: 'A skeleton of a timeline - UNSPECCED & WILL GO AWAY SOON', parameters: { type: 'params', properties: { diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationReports.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationReports.ts index 93ec8bc879d..f3372b96cc8 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationReports.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationReports.ts @@ -12,6 +12,8 @@ import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { subject?: string ignoreSubjects?: string[] + /** Get all reports that were actioned by a specific moderator */ + actionedBy?: string /** Filter reports made by one or more DIDs */ reporters?: string[] resolved?: boolean diff --git a/packages/bsky/src/services/moderation/index.ts b/packages/bsky/src/services/moderation/index.ts index 99715866d2a..4958b9ec8c5 100644 --- a/packages/bsky/src/services/moderation/index.ts +++ b/packages/bsky/src/services/moderation/index.ts @@ -84,6 +84,7 @@ export class ModerationService { ignoreSubjects?: string[] reverse?: boolean reporters?: string[] + actionedBy?: string }): Promise { const { subject, @@ -94,6 +95,7 @@ export class ModerationService { ignoreSubjects, reverse = false, reporters, + actionedBy, } = opts const { ref } = this.db.db.dynamic let builder = this.db.db.selectFrom('moderation_report') @@ -148,8 +150,8 @@ export class ModerationService { ? builder.whereExists(resolutionsQuery) : builder.whereNotExists(resolutionsQuery) } - if (actionType !== undefined) { - const resolutionActionsQuery = this.db.db + if (actionType !== undefined || actionedBy !== undefined) { + let resolutionActionsQuery = this.db.db .selectFrom('moderation_report_resolution') .innerJoin( 'moderation_action', @@ -161,10 +163,22 @@ export class ModerationService { '=', ref('moderation_report.id'), ) - .where('moderation_action.action', '=', sql`${actionType}`) - .where('moderation_action.reversedAt', 'is', null) - .selectAll() - builder = builder.whereExists(resolutionActionsQuery) + + if (actionType) { + resolutionActionsQuery = resolutionActionsQuery + .where('moderation_action.action', '=', sql`${actionType}`) + .where('moderation_action.reversedAt', 'is', null) + } + + if (actionedBy) { + resolutionActionsQuery = resolutionActionsQuery.where( + 'moderation_action.createdBy', + '=', + actionedBy, + ) + } + + builder = builder.whereExists(resolutionActionsQuery.selectAll()) } if (cursor) { const cursorNumeric = parseInt(cursor, 10) diff --git a/packages/pds/src/api/com/atproto/admin/getModerationReports.ts b/packages/pds/src/api/com/atproto/admin/getModerationReports.ts index 171777088c6..ce7cc936e83 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationReports.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationReports.ts @@ -15,6 +15,7 @@ export default function (server: Server, ctx: AppContext) { ignoreSubjects = [], reverse = false, reporters = [], + actionedBy, } = params const moderationService = services.moderation(db) const results = await moderationService.getReports({ @@ -26,6 +27,7 @@ export default function (server: Server, ctx: AppContext) { ignoreSubjects, reverse, reporters, + actionedBy, }) return { encoding: 'application/json', diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 68186fdb49e..3039bef31f8 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -865,6 +865,12 @@ export const schemaDict = { type: 'string', }, }, + actionedBy: { + type: 'string', + format: 'did', + description: + 'Get all reports that were actioned by a specific moderator', + }, reporters: { type: 'array', items: { @@ -6327,7 +6333,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'A skeleton of a timeline', + description: 'A skeleton of a timeline - UNSPECCED & WILL GO AWAY SOON', parameters: { type: 'params', properties: { diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/getModerationReports.ts b/packages/pds/src/lexicon/types/com/atproto/admin/getModerationReports.ts index 93ec8bc879d..f3372b96cc8 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/getModerationReports.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/getModerationReports.ts @@ -12,6 +12,8 @@ import * as ComAtprotoAdminDefs from './defs' export interface QueryParams { subject?: string ignoreSubjects?: string[] + /** Get all reports that were actioned by a specific moderator */ + actionedBy?: string /** Filter reports made by one or more DIDs */ reporters?: string[] resolved?: boolean diff --git a/packages/pds/src/services/moderation/index.ts b/packages/pds/src/services/moderation/index.ts index 18e3ceb5608..97fa178d878 100644 --- a/packages/pds/src/services/moderation/index.ts +++ b/packages/pds/src/services/moderation/index.ts @@ -102,6 +102,7 @@ export class ModerationService { ignoreSubjects?: string[] reverse?: boolean reporters?: string[] + actionedBy?: string }): Promise { const { subject, @@ -112,6 +113,7 @@ export class ModerationService { ignoreSubjects, reverse = false, reporters, + actionedBy, } = opts const { ref } = this.db.db.dynamic let builder = this.db.db.selectFrom('moderation_report') @@ -166,8 +168,8 @@ export class ModerationService { ? builder.whereExists(resolutionsQuery) : builder.whereNotExists(resolutionsQuery) } - if (actionType !== undefined) { - const resolutionActionsQuery = this.db.db + if (actionType !== undefined || actionedBy !== undefined) { + let resolutionActionsQuery = this.db.db .selectFrom('moderation_report_resolution') .innerJoin( 'moderation_action', @@ -179,10 +181,22 @@ export class ModerationService { '=', ref('moderation_report.id'), ) - .where('moderation_action.action', '=', sql`${actionType}`) - .where('moderation_action.reversedAt', 'is', null) - .selectAll() - builder = builder.whereExists(resolutionActionsQuery) + + if (actionType) { + resolutionActionsQuery = resolutionActionsQuery + .where('moderation_action.action', '=', sql`${actionType}`) + .where('moderation_action.reversedAt', 'is', null) + } + + if (actionedBy) { + resolutionActionsQuery = resolutionActionsQuery.where( + 'moderation_action.createdBy', + '=', + actionedBy, + ) + } + + builder = builder.whereExists(resolutionActionsQuery.selectAll()) } if (cursor) { diff --git a/packages/pds/tests/views/admin/__snapshots__/get-moderation-reports.test.ts.snap b/packages/pds/tests/views/admin/__snapshots__/get-moderation-reports.test.ts.snap index c9e984bfb1a..9cfb5ae3c34 100644 --- a/packages/pds/tests/views/admin/__snapshots__/get-moderation-reports.test.ts.snap +++ b/packages/pds/tests/views/admin/__snapshots__/get-moderation-reports.test.ts.snap @@ -1,5 +1,45 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`pds admin get moderation reports view gets all moderation reports actioned by a certain moderator. 1`] = ` +Array [ + Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "id": 1, + "reasonType": "com.atproto.moderation.defs#reasonOther", + "reportedBy": "user(0)", + "resolvedByActionIds": Array [ + 2, + ], + "subject": Object { + "$type": "com.atproto.repo.strongRef", + "cid": "cids(0)", + "uri": "record(0)", + }, + "subjectRepoHandle": "alice.test", + }, +] +`; + +exports[`pds admin get moderation reports view gets all moderation reports actioned by a certain moderator. 2`] = ` +Array [ + Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "id": 3, + "reasonType": "com.atproto.moderation.defs#reasonOther", + "reportedBy": "user(0)", + "resolvedByActionIds": Array [ + 4, + ], + "subject": Object { + "$type": "com.atproto.repo.strongRef", + "cid": "cids(0)", + "uri": "record(0)", + }, + "subjectRepoHandle": "bob.test", + }, +] +`; + exports[`pds admin get moderation reports view gets all moderation reports by active resolution action type. 1`] = ` Array [ Object { diff --git a/packages/pds/tests/views/admin/get-moderation-reports.test.ts b/packages/pds/tests/views/admin/get-moderation-reports.test.ts index 0ef0f92685c..14be4ce821a 100644 --- a/packages/pds/tests/views/admin/get-moderation-reports.test.ts +++ b/packages/pds/tests/views/admin/get-moderation-reports.test.ts @@ -85,6 +85,7 @@ describe('pds admin get moderation reports view', () => { uri: report.subject.uri, cid: report.subject.cid, }, + createdBy: `did:example:admin${i}`, }) if (ab) { await sc.resolveReports({ @@ -241,6 +242,40 @@ describe('pds admin get moderation reports view', () => { expect(forSnapshot(reportsWithTakedown.data.reports)).toMatchSnapshot() }) + it('gets all moderation reports actioned by a certain moderator.', async () => { + const adminDidOne = 'did:example:admin0' + const adminDidTwo = 'did:example:admin2' + const [actionedByAdminOne, actionedByAdminTwo] = await Promise.all([ + agent.api.com.atproto.admin.getModerationReports( + { actionedBy: adminDidOne }, + { headers: { authorization: adminAuth() } }, + ), + agent.api.com.atproto.admin.getModerationReports( + { actionedBy: adminDidTwo }, + { headers: { authorization: adminAuth() } }, + ), + ]) + const [fullReportOne, fullReportTwo] = await Promise.all([ + agent.api.com.atproto.admin.getModerationReport( + { id: actionedByAdminOne.data.reports[0].id }, + { headers: { authorization: adminAuth() } }, + ), + agent.api.com.atproto.admin.getModerationReport( + { id: actionedByAdminTwo.data.reports[0].id }, + { headers: { authorization: adminAuth() } }, + ), + ]) + + expect(forSnapshot(actionedByAdminOne.data.reports)).toMatchSnapshot() + expect(fullReportOne.data.resolvedByActions[0].createdBy).toEqual( + adminDidOne, + ) + expect(forSnapshot(actionedByAdminTwo.data.reports)).toMatchSnapshot() + expect(fullReportTwo.data.resolvedByActions[0].createdBy).toEqual( + adminDidTwo, + ) + }) + it('paginates.', async () => { const results = (results) => results.flatMap((res) => res.reports) const paginator = async (cursor?: string) => { From 670daf5edd80157b7ba155dc6b977d935cf6402b Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Sat, 15 Jul 2023 10:32:45 -0700 Subject: [PATCH 041/237] v0.4.1 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 7353eee0a75..652a971f845 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.4.0", + "version": "0.4.1", "main": "src/index.ts", "scripts": { "codegen": "lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From 1ebda12b9d5c907b25f3f2e2a7f012542b0f1305 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Mon, 17 Jul 2023 10:53:22 -0400 Subject: [PATCH 042/237] Make sequencer leader behavior optional on pds (#1250) * make sequencer leader behavior optional on pds * tidy --- packages/dev-env/src/network.ts | 3 +++ packages/pds/src/config.ts | 12 ++++++++++++ packages/pds/src/context.ts | 4 ++-- packages/pds/src/index.ts | 25 ++++++++----------------- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index 1cce66cfde9..353216b5279 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -62,6 +62,9 @@ export class TestNetwork extends TestNetworkNoAppView { await wait(50) if (!sub) return const state = await sub.getState() + if (!this.pds.ctx.sequencerLeader) { + throw new Error('Sequencer leader not configured on the pds') + } const caughtUp = await this.pds.ctx.sequencerLeader.isCaughtUp() if (!caughtUp) continue const { lastSeq } = await db diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index 4c41c674e4f..992b6ea8e5b 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -58,6 +58,7 @@ export interface ServerConfigValues { maxSubscriptionBuffer: number repoBackfillLimitMs: number sequencerLeaderLockId?: number + sequencerLeaderEnabled?: boolean // this is really only used in test environments dbTxLockNonce?: string @@ -192,6 +193,12 @@ export class ServerConfig { undefined, ) + // by default each instance is a potential sequencer leader, but may be configured off + const sequencerLeaderEnabled = process.env.SEQUENCER_LEADER_ENABLED + ? process.env.SEQUENCER_LEADER_ENABLED !== '0' && + process.env.SEQUENCER_LEADER_ENABLED !== 'false' + : undefined + const dbTxLockNonce = nonemptyString(process.env.DB_TX_LOCK_NONCE) const bskyAppViewEndpoint = nonemptyString( @@ -249,6 +256,7 @@ export class ServerConfig { maxSubscriptionBuffer, repoBackfillLimitMs, sequencerLeaderLockId, + sequencerLeaderEnabled, dbTxLockNonce, bskyAppViewEndpoint, bskyAppViewDid, @@ -460,6 +468,10 @@ export class ServerConfig { return this.cfg.sequencerLeaderLockId } + get sequencerLeaderEnabled() { + return this.cfg.sequencerLeaderEnabled !== false + } + get dbTxLockNonce() { return this.cfg.dbTxLockNonce } diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 9d07e119c98..e811cc68c7d 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -38,7 +38,7 @@ export class AppContext { services: Services messageDispatcher: MessageDispatcher sequencer: Sequencer - sequencerLeader: SequencerLeader + sequencerLeader: SequencerLeader | null labeler: Labeler labelCache: LabelCache backgroundQueue: BackgroundQueue @@ -121,7 +121,7 @@ export class AppContext { return this.opts.sequencer } - get sequencerLeader(): SequencerLeader { + get sequencerLeader(): SequencerLeader | null { return this.opts.sequencerLeader } diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 28ee3b9773d..a349431268d 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -59,11 +59,7 @@ export class PDS { private dbStatsInterval?: NodeJS.Timer private sequencerStatsInterval?: NodeJS.Timer - constructor(opts: { - ctx: AppContext - app: express.Application - sequencerLeader: SequencerLeader - }) { + constructor(opts: { ctx: AppContext; app: express.Application }) { this.ctx = opts.ctx this.app = opts.app } @@ -106,10 +102,9 @@ export class PDS { const messageDispatcher = new MessageDispatcher() const sequencer = new Sequencer(db) - const sequencerLeader = new SequencerLeader( - db, - config.sequencerLeaderLockId, - ) + const sequencerLeader = config.sequencerLeaderEnabled + ? new SequencerLeader(db, config.sequencerLeaderLockId) + : null const mailTransport = config.emailSmtpUrl !== undefined @@ -231,11 +226,7 @@ export class PDS { app.use(server.xrpc.router) app.use(error.handler) - return new PDS({ - ctx, - app, - sequencerLeader, - }) + return new PDS({ ctx, app }) } async start(): Promise { @@ -261,7 +252,7 @@ export class PDS { }, 10000) } this.sequencerStatsInterval = setInterval(() => { - if (this.ctx.sequencerLeader.isLeader) { + if (this.ctx.sequencerLeader?.isLeader) { seqLogger.info( { seq: this.ctx.sequencerLeader.peekSeqVal() }, 'sequencer leader stats', @@ -269,7 +260,7 @@ export class PDS { } }, 500) appviewConsumers.listen(this.ctx) - this.ctx.sequencerLeader.run() + this.ctx.sequencerLeader?.run() await this.ctx.sequencer.start() await this.ctx.db.startListeningToChannels() this.ctx.labelCache.start() @@ -283,7 +274,7 @@ export class PDS { async destroy(): Promise { this.ctx.labelCache.stop() - await this.ctx.sequencerLeader.destroy() + await this.ctx.sequencerLeader?.destroy() await this.terminator?.terminate() await this.ctx.backgroundQueue.destroy() await this.ctx.db.close() From 73a9cd50beeebf38728e83b59d76903707616acc Mon Sep 17 00:00:00 2001 From: Jeff Hodges Date: Mon, 17 Jul 2023 11:51:28 -0700 Subject: [PATCH 043/237] use 127.0.0.1 in with-test-db.sh for colima (#1297) So, since Docker Desktop has licensing issues, some folks use colima for running containers on their macOS machines (The licensing exempted CLI-only version of Docker only exists on Linux). Unfortunately, colima binds host ports only on the IPv4 localhost address (`127.0.0.1`) while the atproto postgres clients will attempt to connect to the IPv6 localhost address (`::1`) that macOS sets in /etc/hosts. See https://github.com/abiosoft/colima/issues/583 and https://github.com/lima-vm/lima/issues/1330 for the tickets against colima. (Docker Desktop binds to both IPv4 and IPv6 localhost addresses and so doesn't have this issue.) To workaround this silly issue, we can use `localhost` within the docker containers and docker-compose, but need to set the `DB_POSTGRES_URL` env var to use the IPv4 localhost explicitly. (Asking folks to edit /etc/hosts causes other tools to break and will be overridden on each OS upgrade.) --- packages/pg/with-test-db.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pg/with-test-db.sh b/packages/pg/with-test-db.sh index 9d8f1b06577..04f11f05852 100755 --- a/packages/pg/with-test-db.sh +++ b/packages/pg/with-test-db.sh @@ -34,7 +34,7 @@ export PGHOST=localhost export PGUSER=pg export PGPASSWORD=password export PGDATABASE=postgres -export DB_POSTGRES_URL="postgresql://pg:password@localhost:5433/postgres" +export DB_POSTGRES_URL="postgresql://pg:password@127.0.0.1:5433/postgres" "$@" code=$? From 8c9becc063be9245648208f2c6e9596166e2aad9 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 17 Jul 2023 14:07:35 -0500 Subject: [PATCH 044/237] Subscription util tests (#1295) * consecutive list tests * flesh out subscription util tests --------- Co-authored-by: Devin Ivy --- packages/bsky/tests/subscription/util.test.ts | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 packages/bsky/tests/subscription/util.test.ts diff --git a/packages/bsky/tests/subscription/util.test.ts b/packages/bsky/tests/subscription/util.test.ts new file mode 100644 index 00000000000..20448b17ef0 --- /dev/null +++ b/packages/bsky/tests/subscription/util.test.ts @@ -0,0 +1,185 @@ +import { wait } from '@atproto/common' +import { + ConsecutiveList, + LatestQueue, + PartitionedQueue, +} from '../../src/subscription/util' +import { randomStr } from '../../../crypto/src' + +describe('subscription utils', () => { + describe('ConsecutiveList', () => { + it('tracks consecutive complete items.', () => { + const consecutive = new ConsecutiveList() + // add items + const item1 = consecutive.push(1) + const item2 = consecutive.push(2) + const item3 = consecutive.push(3) + expect(item1.isComplete).toEqual(false) + expect(item2.isComplete).toEqual(false) + expect(item3.isComplete).toEqual(false) + // complete items out of order + expect(consecutive.list.length).toBe(3) + expect(item2.complete()).toEqual([]) + expect(item2.isComplete).toEqual(true) + expect(consecutive.list.length).toBe(3) + expect(item1.complete()).toEqual([1, 2]) + expect(item1.isComplete).toEqual(true) + expect(consecutive.list.length).toBe(1) + expect(item3.complete()).toEqual([3]) + expect(consecutive.list.length).toBe(0) + expect(item3.isComplete).toEqual(true) + }) + }) + + describe('LatestQueue', () => { + it('only performs most recently queued item.', async () => { + const latest = new LatestQueue() + const complete: number[] = [] + latest.add(async () => { + await wait(1) + complete.push(1) + }) + latest.add(async () => { + await wait(1) + complete.push(2) + }) + latest.add(async () => { + await wait(1) + complete.push(3) + }) + latest.add(async () => { + await wait(1) + complete.push(4) + }) + await latest.queue.onIdle() + expect(complete).toEqual([1, 4]) // skip 2, 3 + latest.add(async () => { + await wait(1) + complete.push(5) + }) + latest.add(async () => { + await wait(1) + complete.push(6) + }) + await latest.queue.onIdle() + expect(complete).toEqual([1, 4, 5, 6]) + }) + + it('stops processing queued messages on destroy.', async () => { + const latest = new LatestQueue() + const complete: number[] = [] + latest.add(async () => { + await wait(1) + complete.push(1) + }) + latest.add(async () => { + await wait(1) + complete.push(2) + }) + const destroyed = latest.destroy() + latest.add(async () => { + await wait(1) + complete.push(3) + }) + await destroyed + expect(complete).toEqual([1]) // 2 was cleared, 3 was after destroy + // show that waiting on destroyed above was already enough to reflect all complete items + await latest.queue.onIdle() + expect(complete).toEqual([1]) + }) + }) + + describe('PartitionedQueue', () => { + it('performs work in parallel across partitions, serial within a partition.', async () => { + const partitioned = new PartitionedQueue({ concurrency: Infinity }) + const complete: number[] = [] + // partition 1 items start slow but get faster: slow should still complete first. + partitioned.add('1', async () => { + await wait(10) + complete.push(11) + }) + partitioned.add('1', async () => { + await wait(5) + complete.push(12) + }) + partitioned.add('1', async () => { + await wait(1) + complete.push(13) + }) + expect(partitioned.partitions.size).toEqual(1) + // partition 2 items complete quickly except the last, which is slowest of all events. + partitioned.add('2', async () => { + await wait(1) + complete.push(21) + }) + partitioned.add('2', async () => { + await wait(1) + complete.push(22) + }) + partitioned.add('2', async () => { + await wait(1) + complete.push(23) + }) + partitioned.add('2', async () => { + await wait(15) + complete.push(24) + }) + expect(partitioned.partitions.size).toEqual(2) + await partitioned.main.onIdle() + expect(complete).toEqual([21, 22, 23, 11, 12, 13, 24]) + expect(partitioned.partitions.size).toEqual(0) + }) + + it('limits overall concurrency.', async () => { + const partitioned = new PartitionedQueue({ concurrency: 1 }) + const complete: number[] = [] + // if concurrency were not constrained, partition 1 would complete all items + // before any items from partition 2. since it is constrained, the work is complete in the order added. + partitioned.add('1', async () => { + await wait(1) + complete.push(11) + }) + partitioned.add('2', async () => { + await wait(10) + complete.push(21) + }) + partitioned.add('1', async () => { + await wait(1) + complete.push(12) + }) + partitioned.add('2', async () => { + await wait(10) + complete.push(22) + }) + // only partition 1 exists so far due to the concurrency + expect(partitioned.partitions.size).toEqual(1) + await partitioned.main.onIdle() + expect(complete).toEqual([11, 21, 12, 22]) + expect(partitioned.partitions.size).toEqual(0) + }) + + it('settles with many items.', async () => { + const partitioned = new PartitionedQueue({ concurrency: 100 }) + const complete: { partition: string; id: number }[] = [] + const partitions = new Set() + for (let i = 0; i < 500; ++i) { + const partition = randomStr(1, 'base16').slice(0, 1) + partitions.add(partition) + partitioned.add(partition, async () => { + await wait((i % 2) * 2) + complete.push({ partition, id: i }) + }) + } + expect(partitioned.partitions.size).toEqual(partitions.size) + await partitioned.main.onIdle() + expect(complete.length).toEqual(500) + for (const partition of partitions) { + const ids = complete + .filter((item) => item.partition === partition) + .map((item) => item.id) + expect(ids).toEqual([...ids].sort((a, b) => a - b)) + } + expect(partitioned.partitions.size).toEqual(0) + }) + }) +}) From c9acdd39a2cd6163807868c8a8b621511d1456b7 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 17 Jul 2023 16:39:01 -0500 Subject: [PATCH 045/237] Content reporting on record fields (#1351) * content reporting on record fields * fix test * tests * tidy --- .../api/com/atproto/identity/updateHandle.ts | 5 +- .../api/com/atproto/server/createAccount.ts | 5 +- packages/pds/src/config.ts | 24 +-- .../src/content-reporter/explicit-slurs.ts | 21 +++ packages/pds/src/content-reporter/index.ts | 88 +++++++++++ .../validator.ts | 2 +- packages/pds/src/context.ts | 6 + packages/pds/src/handle/index.ts | 4 +- packages/pds/src/handle/moderation/index.ts | 66 --------- packages/pds/src/index.ts | 21 +++ packages/pds/src/repo/prepare.ts | 17 +++ packages/pds/src/services/index.ts | 4 + packages/pds/src/services/repo/index.ts | 25 ++-- packages/pds/tests/content-reporter.test.ts | 138 ++++++++++++++++++ packages/pds/tests/handle-validation.test.ts | 4 +- 15 files changed, 329 insertions(+), 101 deletions(-) create mode 100644 packages/pds/src/content-reporter/explicit-slurs.ts create mode 100644 packages/pds/src/content-reporter/index.ts rename packages/pds/src/{handle/moderation => content-reporter}/validator.ts (98%) delete mode 100644 packages/pds/src/handle/moderation/index.ts create mode 100644 packages/pds/tests/content-reporter.test.ts diff --git a/packages/pds/src/api/com/atproto/identity/updateHandle.ts b/packages/pds/src/api/com/atproto/identity/updateHandle.ts index 356165e72f7..7d07dc807fa 100644 --- a/packages/pds/src/api/com/atproto/identity/updateHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/updateHandle.ts @@ -7,7 +7,6 @@ import { UserAlreadyExistsError, } from '../../../../services/account' import { httpLogger } from '../../../../logger' -import { backgroundHandleCheckForFlag } from '../../../../handle/moderation' export default function (server: Server, ctx: AppContext) { server.com.atproto.identity.updateHandle({ @@ -55,9 +54,7 @@ export default function (server: Server, ctx: AppContext) { ) } - if (ctx.cfg.unacceptableHandleWordsB64) { - backgroundHandleCheckForFlag({ ctx, handle, did: requester }) - } + ctx.contentReporter?.checkHandle({ handle, did: requester }) }, }) } diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 2b0e5f72224..6965eb596a0 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -9,7 +9,6 @@ import { UserAlreadyExistsError } from '../../../../services/account' import AppContext from '../../../../context' import Database from '../../../../db' import { AtprotoData } from '@atproto/identity' -import { backgroundHandleCheckForFlag } from '../../../../handle/moderation' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.createAccount(async ({ input, req }) => { @@ -106,9 +105,7 @@ export default function (server: Server, ctx: AppContext) { } }) - if (ctx.cfg.unacceptableHandleWordsB64) { - backgroundHandleCheckForFlag({ ctx, handle, did: result.did }) - } + ctx.contentReporter?.checkHandle({ handle, did: result.did }) return { encoding: 'application/json', diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index 992b6ea8e5b..4c15fa64b83 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -50,8 +50,8 @@ export interface ServerConfigValues { hiveApiKey?: string labelerDid: string labelerKeywords: Record - unacceptableHandleWordsB64?: string - falsePositiveHandleWordsB64?: string + unacceptableWordsB64?: string + falsePositiveWordsB64?: string feedGenDid?: string @@ -166,11 +166,11 @@ export class ServerConfig { const labelerDid = process.env.LABELER_DID || 'did:example:labeler' const labelerKeywords = {} - const unacceptableHandleWordsB64 = nonemptyString( - process.env.UNACCEPTABLE_HANDLE_WORDS_B64, + const unacceptableWordsB64 = nonemptyString( + process.env.UNACCEPTABLE_WORDS_B64, ) - const falsePositiveHandleWordsB64 = nonemptyString( - process.env.FALSE_POSITIVE_HANDLE_WORDS_B64, + const falsePositiveWordsB64 = nonemptyString( + process.env.FALSE_POSITIVE_WORDS_B64, ) const feedGenDid = process.env.FEED_GEN_DID @@ -250,8 +250,8 @@ export class ServerConfig { hiveApiKey, labelerDid, labelerKeywords, - unacceptableHandleWordsB64, - falsePositiveHandleWordsB64, + unacceptableWordsB64, + falsePositiveWordsB64, feedGenDid, maxSubscriptionBuffer, repoBackfillLimitMs, @@ -444,12 +444,12 @@ export class ServerConfig { return this.cfg.labelerKeywords } - get unacceptableHandleWordsB64() { - return this.cfg.unacceptableHandleWordsB64 + get unacceptableWordsB64() { + return this.cfg.unacceptableWordsB64 } - get falsePositiveHandleWordsB64() { - return this.cfg.falsePositiveHandleWordsB64 + get falsePositiveWordsB64() { + return this.cfg.falsePositiveWordsB64 } get feedGenDid() { diff --git a/packages/pds/src/content-reporter/explicit-slurs.ts b/packages/pds/src/content-reporter/explicit-slurs.ts new file mode 100644 index 00000000000..534091366f6 --- /dev/null +++ b/packages/pds/src/content-reporter/explicit-slurs.ts @@ -0,0 +1,21 @@ +// regexes taken from: https://github.com/Blank-Cheque/Slurs +/* eslint-disable no-misleading-character-class */ +const explicitSlurRegexes = [ + /\b[cĆćĈĉČčĊċÇçḈḉȻȼꞒꞓꟄꞔƇƈɕ][hĤĥȞȟḦḧḢḣḨḩḤḥḪḫH̱ẖĦħⱧⱨꞪɦꞕΗНн][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn][kḰḱǨǩĶķḲḳḴḵƘƙⱩⱪᶄꝀꝁꝂꝃꝄꝅꞢꞣ][sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/, + /\b[cĆćĈĉČčĊċÇçḈḉȻȼꞒꞓꟄꞔƇƈɕ][ÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOo0]{2}[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn][sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/, + /\b[fḞḟƑƒꞘꞙᵮᶂ][aÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa@4][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶGgqꝖꝗꝘꝙɋʠ]{1,2}([ÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOo0e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEeiÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][tŤťṪṫŢţṬṭȚțṰṱṮṯŦŧȾⱦƬƭƮʈT̈ẗᵵƫȶ]{1,2}([rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][yÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ]|[rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe])?)?[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/, + /\b[kḰḱǨǩĶķḲḳḴḵƘƙⱩⱪᶄꝀꝁꝂꝃꝄꝅꞢꞣ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLlyÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ][kḰḱǨǩĶķḲḳḴḵƘƙⱩⱪᶄꝀꝁꝂꝃꝄꝅꞢꞣ][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe]([rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][yÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ]|[rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe])?[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]*\b/, + /\b[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLloÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOoІіa4ÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶGgqꝖꝗꝘꝙɋʠ]{2}(l[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe]t|[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEeaÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ]?|n[ÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOo0][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶGgqꝖꝗꝘꝙɋʠ]|[a4ÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa]?)?[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/, + /[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLloÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOoІіa4ÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶGgqꝖꝗꝘꝙɋʠ]{2}(l[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe]t|[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ])[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?/, + /\b[tŤťṪṫŢţṬṭȚțṰṱṮṯŦŧȾⱦƬƭƮʈT̈ẗᵵƫȶ][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][aÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa4]+[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn]{1,2}([iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe]|[yÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ]|[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ])[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/, +] + +export const hasExplicitSlur = (handle: string): boolean => { + return explicitSlurRegexes.some( + (reg) => + reg.test(handle) || + reg.test( + handle.replaceAll('.', '').replaceAll('-', '').replaceAll('_', ''), + ), + ) +} diff --git a/packages/pds/src/content-reporter/index.ts b/packages/pds/src/content-reporter/index.ts new file mode 100644 index 00000000000..50088c88a11 --- /dev/null +++ b/packages/pds/src/content-reporter/index.ts @@ -0,0 +1,88 @@ +import { AtUri } from '@atproto/uri' +import { RepoRecord } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' +import * as ui8 from 'uint8arrays' +import { UnacceptableWordValidator } from './validator' +import { REASONOTHER } from '../lexicon/types/com/atproto/moderation/defs' +import { isRecord as isList } from '../lexicon/types/app/bsky/graph/list' +import { isRecord as isProfile } from '../lexicon/types/app/bsky/actor/profile' +import { isRecord as isFeedGenerator } from '../lexicon/types/app/bsky/feed/generator' +import { BackgroundQueue } from '../event-stream/background-queue' +import { ModerationService } from '../services/moderation' + +export class ContentReporter { + backgroundQueue: BackgroundQueue + moderationService: ModerationService + reporterDid: string + validator: UnacceptableWordValidator + + constructor(opts: { + backgroundQueue: BackgroundQueue + moderationService: ModerationService + reporterDid: string + unacceptableB64: string + falsePositivesB64?: string + }) { + this.backgroundQueue = opts.backgroundQueue + this.moderationService = opts.moderationService + this.reporterDid = opts.reporterDid + this.validator = new UnacceptableWordValidator( + decode(opts.unacceptableB64), + opts.falsePositivesB64 ? decode(opts.falsePositivesB64) : undefined, + ) + } + + checkHandle(opts: { handle: string; did: string }) { + const { handle, did } = opts + return this.checkContent({ + content: handle, + subject: { did }, + }) + } + + checkRecord(opts: { record: RepoRecord; uri: AtUri; cid: CID }) { + const { record, uri, cid } = opts + let content = uri.rkey + if (isProfile(record)) { + content += ' ' + record.displayName + } else if (isList(record)) { + content += ' ' + record.name + } else if (isFeedGenerator(record)) { + content += ' ' + record.displayName + } + + return this.checkContent({ + content, + subject: { uri, cid }, + }) + } + + checkContent(opts: { + content: string + subject: { did: string } | { uri: AtUri; cid?: CID } + }) { + const { content, subject } = opts + const possibleSlurs = this.validator.getMatches(content) + if (possibleSlurs.length < 1) { + return + } + this.backgroundQueue.add(async () => { + await this.moderationService.report({ + reasonType: REASONOTHER, + reason: `Automatically flagged for possible slurs: ${possibleSlurs.join( + ', ', + )}`, + subject, + reportedBy: this.reporterDid, + }) + }) + } +} + +export const decode = (encoded: string): string[] => { + return ui8.toString(ui8.fromString(encoded, 'base64'), 'utf8').split(',') +} + +export const encode = (words: string[]): string => { + return ui8.toString(ui8.fromString(words.join(','), 'utf8'), 'base64') +} diff --git a/packages/pds/src/handle/moderation/validator.ts b/packages/pds/src/content-reporter/validator.ts similarity index 98% rename from packages/pds/src/handle/moderation/validator.ts rename to packages/pds/src/content-reporter/validator.ts index d832e828334..9f5b5689e4a 100644 --- a/packages/pds/src/handle/moderation/validator.ts +++ b/packages/pds/src/content-reporter/validator.ts @@ -1,6 +1,6 @@ import { dedupeStrs } from '@atproto/common' -export class UnacceptableHandleValidator { +export class UnacceptableWordValidator { private bannedWords: Set private falsePositives: Set diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index e811cc68c7d..4aa74e45156 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -19,6 +19,7 @@ import DidSqlCache from './did-cache' import { MountedAlgos } from './feed-gen/types' import { Crawlers } from './crawlers' import { LabelCache } from './label-cache' +import { ContentReporter } from './content-reporter' export class AppContext { private _appviewAgent: AtpAgent | null @@ -41,6 +42,7 @@ export class AppContext { sequencerLeader: SequencerLeader | null labeler: Labeler labelCache: LabelCache + contentReporter?: ContentReporter backgroundQueue: BackgroundQueue crawlers: Crawlers algos: MountedAlgos @@ -133,6 +135,10 @@ export class AppContext { return this.opts.labelCache } + get contentReporter(): ContentReporter | undefined { + return this.opts.contentReporter + } + get backgroundQueue(): BackgroundQueue { return this.opts.backgroundQueue } diff --git a/packages/pds/src/handle/index.ts b/packages/pds/src/handle/index.ts index edad6c736e5..d847df1cd45 100644 --- a/packages/pds/src/handle/index.ts +++ b/packages/pds/src/handle/index.ts @@ -1,7 +1,7 @@ import * as ident from '@atproto/identifier' import { InvalidRequestError } from '@atproto/xrpc-server' import { reservedSubdomains } from './reserved' -import { hasExplicitSlur } from './moderation' +import { hasExplicitSlur } from '../content-reporter/explicit-slurs' import AppContext from '../context' export const normalizeAndValidateHandle = async (opts: { @@ -20,7 +20,7 @@ export const normalizeAndValidateHandle = async (opts: { 'InvalidHandle', ) } - // slur check etc + // slur check if (hasExplicitSlur(handle)) { throw new InvalidRequestError( 'Inappropriate language in handle', diff --git a/packages/pds/src/handle/moderation/index.ts b/packages/pds/src/handle/moderation/index.ts deleted file mode 100644 index 3bbf013e8c6..00000000000 --- a/packages/pds/src/handle/moderation/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as ui8 from 'uint8arrays' -import AppContext from '../../context' -import { REASONOTHER } from '../../lexicon/types/com/atproto/moderation/defs' -import { UnacceptableHandleValidator } from './validator' - -// regexes taken from: https://github.com/Blank-Cheque/Slurs -/* eslint-disable no-misleading-character-class */ -const explicitSlurRegexes = [ - /\b[cĆćĈĉČčĊċÇçḈḉȻȼꞒꞓꟄꞔƇƈɕ][hĤĥȞȟḦḧḢḣḨḩḤḥḪḫH̱ẖĦħⱧⱨꞪɦꞕΗНн][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn][kḰḱǨǩĶķḲḳḴḵƘƙⱩⱪᶄꝀꝁꝂꝃꝄꝅꞢꞣ][sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/, - /\b[cĆćĈĉČčĊċÇçḈḉȻȼꞒꞓꟄꞔƇƈɕ][ÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOo0]{2}[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn][sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/, - /[fḞḟƑƒꞘꞙᵮᶂ][aÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa@4][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶGgqꝖꝗꝘꝙɋʠ]{1,2}([ÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOo0e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEeiÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][tŤťṪṫŢţṬṭȚțṰṱṮṯŦŧȾⱦƬƭƮʈT̈ẗᵵƫȶ]{1,2}([rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][yÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ]|[rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe])?)?[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/, - /\b[kḰḱǨǩĶķḲḳḴḵƘƙⱩⱪᶄꝀꝁꝂꝃꝄꝅꞢꞣ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLlyÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ][kḰḱǨǩĶķḲḳḴḵƘƙⱩⱪᶄꝀꝁꝂꝃꝄꝅꞢꞣ][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe]([rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][yÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ]|[rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe])?[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]*\b/, - /\b([sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ][a4ÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa][nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn][dĎďḊḋḐḑD̦d̦ḌḍḒḓḎḏĐđÐðƉɖƊɗᵭᶁᶑȡ])?[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn][iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLloÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOoІіa4ÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶGgqꝖꝗꝘꝙɋʠ]{1,2}(l[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe]t|[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEeaÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ]?|n[ÓóÒòŎŏÔôỐốỒồỖỗỔổǑǒÖöȪȫŐőÕõṌṍṎṏȬȭȮȯO͘o͘ȰȱØøǾǿǪǫǬǭŌōṒṓṐṑỎỏȌȍȎȏƠơỚớỜờỠỡỞởỢợỌọỘộO̩o̩Ò̩ò̩Ó̩ó̩ƟɵꝊꝋꝌꝍⱺOo0][gǴǵĞğĜĝǦǧĠġG̃g̃ĢģḠḡǤǥꞠꞡƓɠᶃꬶGgqꝖꝗꝘꝙɋʠ]|[a4ÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa]?)?[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/, - /\b[tŤťṪṫŢţṬṭȚțṰṱṮṯŦŧȾⱦƬƭƮʈT̈ẗᵵƫȶ][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ][aÁáÀàĂăẮắẰằẴẵẲẳÂâẤấẦầẪẫẨẩǍǎÅåǺǻÄäǞǟÃãȦȧǠǡĄąĄ́ą́Ą̃ą̃ĀāĀ̀ā̀ẢảȀȁA̋a̋ȂȃẠạẶặẬậḀḁȺⱥꞺꞻᶏẚAa4]+[nŃńǸǹŇňÑñṄṅŅņṆṇṊṋṈṉN̈n̈ƝɲŊŋꞐꞑꞤꞥᵰᶇɳȵꬻꬼИиПпNn]{1,2}([iÍíi̇́Ììi̇̀ĬĭÎîǏǐÏïḮḯĨĩi̇̃ĮįĮ́į̇́Į̃į̇̃ĪīĪ̀ī̀ỈỉȈȉI̋i̋ȊȋỊịꞼꞽḬḭƗɨᶖİiIıIi1lĺľļḷḹl̃ḽḻłŀƚꝉⱡɫɬꞎꬷꬸꬹᶅɭȴLl][e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe]|[yÝýỲỳŶŷY̊ẙŸÿỸỹẎẏȲȳỶỷỴỵɎɏƳƴỾỿ]|[e3ЄєЕеÉéÈèĔĕÊêẾếỀềỄễỂểÊ̄ê̄Ê̌ê̌ĚěËëẼẽĖėĖ́ė́Ė̃ė̃ȨȩḜḝĘęĘ́ę́Ę̃ę̃ĒēḖḗḔḕẺẻȄȅE̋e̋ȆȇẸẹỆệḘḙḚḛɆɇE̩e̩È̩è̩É̩é̩ᶒⱸꬴꬳEe][rŔŕŘřṘṙŖŗȐȑȒȓṚṛṜṝṞṟR̃r̃ɌɍꞦꞧⱤɽᵲᶉꭉ])[sŚśṤṥŜŝŠšṦṧṠṡŞşṢṣṨṩȘșS̩s̩ꞨꞩⱾȿꟅʂᶊᵴ]?\b/, -] - -export const hasExplicitSlur = (handle: string): boolean => { - return explicitSlurRegexes.some((reg) => reg.test(handle)) -} - -const decode = (encoded: string): string[] => { - return ui8.toString(ui8.fromString(encoded, 'base64'), 'utf8').split(',') -} - -let _validator: UnacceptableHandleValidator | undefined = undefined -const getValidator = ( - unacceptable: string, - falsePositives?: string, -): UnacceptableHandleValidator => { - if (!_validator) { - _validator = new UnacceptableHandleValidator( - decode(unacceptable), - falsePositives ? decode(falsePositives) : undefined, - ) - } - return _validator -} - -export const backgroundHandleCheckForFlag = (opts: { - ctx: AppContext - handle: string - did: string -}) => { - const { ctx, handle, did } = opts - if (!ctx.cfg.unacceptableHandleWordsB64) { - return - } - const validator = getValidator( - ctx.cfg.unacceptableHandleWordsB64, - ctx.cfg.falsePositiveHandleWordsB64, - ) - const possibleSlurs = validator.getMatches(handle) - if (possibleSlurs.length < 1) { - return - } - ctx.backgroundQueue.add(async () => { - await ctx.services.moderation(ctx.db).report({ - reasonType: REASONOTHER, - reason: `Automatically flagged for possible slurs: ${possibleSlurs.join( - ', ', - )}`, - subject: { did }, - reportedBy: ctx.cfg.serverDid, - }) - }) -} diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index a349431268d..33a93eb35f5 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -42,6 +42,8 @@ import DidSqlCache from './did-cache' import { MountedAlgos } from './feed-gen/types' import { Crawlers } from './crawlers' import { LabelCache } from './label-cache' +import { ContentReporter } from './content-reporter' +import { ModerationService } from './services/moderation' export type { ServerConfigValues } from './config' export { ServerConfig } from './config' @@ -175,6 +177,23 @@ export class PDS { const labelCache = new LabelCache(db) + let contentReporter: ContentReporter | undefined = undefined + if (config.unacceptableWordsB64) { + contentReporter = new ContentReporter({ + backgroundQueue, + moderationService: new ModerationService( + db, + messageDispatcher, + blobstore, + imgUriBuilder, + imgInvalidator, + ), + reporterDid: config.labelerDid, + unacceptableB64: config.unacceptableWordsB64, + falsePositivesB64: config.falsePositiveWordsB64, + }) + } + const services = createServices({ repoSigningKey, messageDispatcher, @@ -183,6 +202,7 @@ export class PDS { imgInvalidator, labeler, labelCache, + contentReporter, backgroundQueue, crawlers, }) @@ -201,6 +221,7 @@ export class PDS { sequencerLeader, labeler, labelCache, + contentReporter, services, mailer, imgUriBuilder, diff --git a/packages/pds/src/repo/prepare.ts b/packages/pds/src/repo/prepare.ts index 875b83e76bf..af4dbbc9365 100644 --- a/packages/pds/src/repo/prepare.ts +++ b/packages/pds/src/repo/prepare.ts @@ -33,6 +33,7 @@ import { } from '../lexicon/types/app/bsky/feed/post' import { isRecord as isList } from '../lexicon/types/app/bsky/graph/list' import { isRecord as isProfile } from '../lexicon/types/app/bsky/actor/profile' +import { hasExplicitSlur } from '../content-reporter/explicit-slurs' // @TODO do this dynamically off of schemas export const blobsForWrite = (record: unknown): PreparedBlobRef[] => { @@ -154,6 +155,7 @@ export const prepareCreate = async (opts: { assertValidRecord(record) } const rkey = opts.rkey || TID.nextStr() + assertNoExplicitSlurs(rkey, record) return { action: WriteOpAction.Create, uri: AtUri.make(did, collection, rkey), @@ -177,6 +179,7 @@ export const prepareUpdate = async (opts: { if (validate) { assertValidRecord(record) } + assertNoExplicitSlurs(rkey, record) return { action: WriteOpAction.Update, uri: AtUri.make(did, collection, rkey), @@ -256,3 +259,17 @@ async function cidForSafeRecord(record: RepoRecord) { throw badRecordErr } } + +function assertNoExplicitSlurs(rkey: string, record: RepoRecord) { + let toCheck = rkey + if (isProfile(record)) { + toCheck += ' ' + record.displayName + } else if (isList(record)) { + toCheck += ' ' + record.name + } else if (isFeedGenerator(record)) { + toCheck += ' ' + record.displayName + } + if (hasExplicitSlur(toCheck)) { + throw new InvalidRecordError('Unacceptable slur in record') + } +} diff --git a/packages/pds/src/services/index.ts b/packages/pds/src/services/index.ts index f89fd917082..528c6de5165 100644 --- a/packages/pds/src/services/index.ts +++ b/packages/pds/src/services/index.ts @@ -18,6 +18,7 @@ import { LabelService } from '../app-view/services/label' import { BackgroundQueue } from '../event-stream/background-queue' import { Crawlers } from '../crawlers' import { LabelCache } from '../label-cache' +import { ContentReporter } from '../content-reporter' export function createServices(resources: { repoSigningKey: crypto.Keypair @@ -27,6 +28,7 @@ export function createServices(resources: { imgInvalidator: ImageInvalidator labeler: Labeler labelCache: LabelCache + contentReporter?: ContentReporter backgroundQueue: BackgroundQueue crawlers: Crawlers }): Services { @@ -38,6 +40,7 @@ export function createServices(resources: { imgInvalidator, labeler, labelCache, + contentReporter, backgroundQueue, crawlers, } = resources @@ -52,6 +55,7 @@ export function createServices(resources: { backgroundQueue, crawlers, labeler, + contentReporter, ), moderation: ModerationService.creator( messageDispatcher, diff --git a/packages/pds/src/services/repo/index.ts b/packages/pds/src/services/repo/index.ts index 7dc1dac4252..5e9a3116d3b 100644 --- a/packages/pds/src/services/repo/index.ts +++ b/packages/pds/src/services/repo/index.ts @@ -28,6 +28,7 @@ import { wait } from '@atproto/common' import { BackgroundQueue } from '../../event-stream/background-queue' import { countAll } from '../../db/util' import { Crawlers } from '../../crawlers' +import { ContentReporter } from '../../content-reporter' export class RepoService { blobs: RepoBlobs @@ -40,6 +41,7 @@ export class RepoService { public backgroundQueue: BackgroundQueue, public crawlers: Crawlers, public labeler: Labeler, + public contentReporter?: ContentReporter, ) { this.blobs = new RepoBlobs(db, blobstore, backgroundQueue) } @@ -51,6 +53,7 @@ export class RepoService { backgroundQueue: BackgroundQueue, crawlers: Crawlers, labeler: Labeler, + contentReporter?: ContentReporter, ) { return (db: Database) => new RepoService( @@ -61,6 +64,7 @@ export class RepoService { backgroundQueue, crawlers, labeler, + contentReporter, ) } @@ -81,6 +85,7 @@ export class RepoService { this.backgroundQueue, this.crawlers, this.labeler, + this.contentReporter, ) return fn(srvc) }) @@ -231,20 +236,20 @@ export class RepoService { this.backgroundQueue.add(async () => { await this.crawlers.notifyOfUpdate() }) + writes.forEach((write) => { + if ( + write.action === WriteOpAction.Create || + write.action === WriteOpAction.Update + ) { + // @TODO move to appview + this.labeler.processRecord(write.uri, write.record) + this.contentReporter?.checkRecord(write) + } + }) }) const seqEvt = await sequencer.formatSeqCommit(did, commitData, writes) await sequencer.sequenceEvt(this.db, seqEvt) - - // @TODO move to appview - writes.forEach((write) => { - if ( - write.action === WriteOpAction.Create || - write.action === WriteOpAction.Update - ) { - this.labeler.processRecord(write.uri, write.record) - } - }) } async rebaseRepo(did: string, swapCommit?: CID) { diff --git a/packages/pds/tests/content-reporter.test.ts b/packages/pds/tests/content-reporter.test.ts new file mode 100644 index 00000000000..e4a25d68f05 --- /dev/null +++ b/packages/pds/tests/content-reporter.test.ts @@ -0,0 +1,138 @@ +import { encode } from '../src/content-reporter' +import { TestNetworkNoAppView } from '@atproto/dev-env' +import { SeedClient } from './seeds/client' +import basicSeed from './seeds/basic' +import { AtpAgent } from '@atproto/api' + +describe('content reporter', () => { + let network: TestNetworkNoAppView + let agent: AtpAgent + let sc: SeedClient + + let alice: string + + beforeAll(async () => { + network = await TestNetworkNoAppView.create({ + dbPostgresSchema: 'content_reporter', + pds: { + unacceptableWordsB64: encode(['evil']), + }, + }) + agent = network.pds.getClient() + sc = new SeedClient(agent) + await basicSeed(sc) + await network.processAll() + alice = sc.dids.alice + }) + + afterAll(async () => { + await network.close() + }) + + const getAllReports = () => { + return network.pds.ctx.db.db + .selectFrom('moderation_report') + .selectAll() + .orderBy('id', 'asc') + .execute() + } + + it('doesnt label any of the content in the seed', async () => { + const reports = await getAllReports() + expect(reports.length).toBe(0) + }) + + it('flags a handle with an unacceptable word', async () => { + await sc.updateHandle(alice, 'evil.test') + await network.processAll() + const reports = await getAllReports() + expect(reports.length).toBe(1) + expect(reports.at(-1)?.subjectDid).toEqual(alice) + }) + + it('flags a profile with an unacceptable displayName', async () => { + const res = await agent.api.com.atproto.repo.putRecord( + { + repo: alice, + collection: 'app.bsky.actor.profile', + rkey: 'self', + record: { + displayName: 'evil alice', + }, + }, + { headers: sc.getHeaders(alice), encoding: 'application/json' }, + ) + await network.pds.ctx.backgroundQueue.processAll() + + const reports = await getAllReports() + expect(reports.length).toBe(2) + expect(reports.at(-1)?.subjectUri).toEqual(res.data.uri) + expect(reports.at(-1)?.subjectCid).toEqual(res.data.cid) + }) + + it('flags a list with an unacceptable name', async () => { + const res = await agent.api.com.atproto.repo.createRecord( + { + repo: alice, + collection: 'app.bsky.graph.list', + rkey: 'list', + record: { + name: 'myevillist', + purpose: 'app.bsky.graph.defs#modList', + createdAt: new Date().toISOString(), + }, + }, + { headers: sc.getHeaders(alice), encoding: 'application/json' }, + ) + await network.processAll() + + const reports = await getAllReports() + expect(reports.length).toBe(3) + expect(reports.at(-1)?.subjectUri).toEqual(res.data.uri) + expect(reports.at(-1)?.subjectCid).toEqual(res.data.cid) + }) + + it('flags a feed generator with an unacceptable displayName', async () => { + const res = await agent.api.com.atproto.repo.createRecord( + { + repo: alice, + collection: 'app.bsky.feed.generator', + rkey: 'generator', + record: { + did: alice, + displayName: 'myevilfeed', + createdAt: new Date().toISOString(), + }, + }, + { headers: sc.getHeaders(alice), encoding: 'application/json' }, + ) + await network.processAll() + + const reports = await getAllReports() + expect(reports.length).toBe(4) + expect(reports.at(-1)?.subjectUri).toEqual(res.data.uri) + expect(reports.at(-1)?.subjectCid).toEqual(res.data.cid) + }) + + it('flags a record with an unacceptable rkey', async () => { + const res = await agent.api.com.atproto.repo.createRecord( + { + repo: alice, + collection: 'app.bsky.feed.generator', + rkey: 'evilrkey', + record: { + did: alice, + displayName: 'totally fine feed', + createdAt: new Date().toISOString(), + }, + }, + { headers: sc.getHeaders(alice), encoding: 'application/json' }, + ) + await network.processAll() + + const reports = await getAllReports() + expect(reports.length).toBe(5) + expect(reports.at(-1)?.subjectUri).toEqual(res.data.uri) + expect(reports.at(-1)?.subjectCid).toEqual(res.data.cid) + }) +}) diff --git a/packages/pds/tests/handle-validation.test.ts b/packages/pds/tests/handle-validation.test.ts index c0174fb533a..14b9b2d2646 100644 --- a/packages/pds/tests/handle-validation.test.ts +++ b/packages/pds/tests/handle-validation.test.ts @@ -1,6 +1,6 @@ import { isValidTld } from '@atproto/identifier' import { ensureHandleServiceConstraints } from '../src/handle' -import { UnacceptableHandleValidator } from '../src/handle/moderation/validator' +import { UnacceptableWordValidator } from '../src/content-reporter/validator' describe('handle validation', () => { it('validates service constraints', () => { @@ -27,7 +27,7 @@ describe('handle validation', () => { expect(isValidTld('atproto.internal')).toBe(false) }) - const validator = new UnacceptableHandleValidator( + const validator = new UnacceptableWordValidator( ['evil', 'mean', 'bad'], ['baddie'], ) From 47bf80646f0dbe86051269b8a6c3b50053dba04d Mon Sep 17 00:00:00 2001 From: devin ivy Date: Mon, 17 Jul 2023 18:38:05 -0400 Subject: [PATCH 046/237] Check rkey contents just for non-tids (#1353) check rkey content for non-tids --- packages/pds/src/content-reporter/index.ts | 3 ++- packages/pds/src/repo/prepare.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/pds/src/content-reporter/index.ts b/packages/pds/src/content-reporter/index.ts index 50088c88a11..659ab5f1e13 100644 --- a/packages/pds/src/content-reporter/index.ts +++ b/packages/pds/src/content-reporter/index.ts @@ -42,12 +42,13 @@ export class ContentReporter { checkRecord(opts: { record: RepoRecord; uri: AtUri; cid: CID }) { const { record, uri, cid } = opts - let content = uri.rkey + let content = '' if (isProfile(record)) { content += ' ' + record.displayName } else if (isList(record)) { content += ' ' + record.name } else if (isFeedGenerator(record)) { + content += ' ' + uri.rkey content += ' ' + record.displayName } diff --git a/packages/pds/src/repo/prepare.ts b/packages/pds/src/repo/prepare.ts index af4dbbc9365..7a3e9afea05 100644 --- a/packages/pds/src/repo/prepare.ts +++ b/packages/pds/src/repo/prepare.ts @@ -261,12 +261,13 @@ async function cidForSafeRecord(record: RepoRecord) { } function assertNoExplicitSlurs(rkey: string, record: RepoRecord) { - let toCheck = rkey + let toCheck = '' if (isProfile(record)) { toCheck += ' ' + record.displayName } else if (isList(record)) { toCheck += ' ' + record.name } else if (isFeedGenerator(record)) { + toCheck += ' ' + rkey toCheck += ' ' + record.displayName } if (hasExplicitSlur(toCheck)) { From 775944e84afde93cb4d7131e5e36d075d1ccb11a Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 18 Jul 2023 01:06:44 +0200 Subject: [PATCH 047/237] :sparkles: Added new procedure for sending admin email (#1312) * :construction: Added new lexicon for sending admin email * :sparkles: Add moderation mailer * :sparkles: Switch to text email content from html * :broom: Cleanup some early implementation code and reflect PR reivew * :sparkles: Use smtp host instead of gmail service config * :sparkles: Move to using single smtp url --- lexicons/com/atproto/admin/sendEmail.json | 32 +++++++++++++ packages/api/src/client/index.ts | 13 +++++ packages/api/src/client/lexicons.ts | 42 ++++++++++++++++ .../types/com/atproto/admin/sendEmail.ts | 40 ++++++++++++++++ packages/bsky/src/lexicon/index.ts | 8 ++++ packages/bsky/src/lexicon/lexicons.ts | 42 ++++++++++++++++ .../types/com/atproto/admin/sendEmail.ts | 48 +++++++++++++++++++ .../pds/src/api/com/atproto/admin/index.ts | 2 + .../src/api/com/atproto/admin/sendEmail.ts | 38 +++++++++++++++ packages/pds/src/config.ts | 17 +++++++ packages/pds/src/context.ts | 6 +++ packages/pds/src/index.ts | 15 +++++- packages/pds/src/lexicon/index.ts | 8 ++++ packages/pds/src/lexicon/lexicons.ts | 42 ++++++++++++++++ .../types/com/atproto/admin/sendEmail.ts | 48 +++++++++++++++++++ packages/pds/src/mailer/moderation.ts | 34 +++++++++++++ 16 files changed, 433 insertions(+), 2 deletions(-) create mode 100644 lexicons/com/atproto/admin/sendEmail.json create mode 100644 packages/api/src/client/types/com/atproto/admin/sendEmail.ts create mode 100644 packages/bsky/src/lexicon/types/com/atproto/admin/sendEmail.ts create mode 100644 packages/pds/src/api/com/atproto/admin/sendEmail.ts create mode 100644 packages/pds/src/lexicon/types/com/atproto/admin/sendEmail.ts create mode 100644 packages/pds/src/mailer/moderation.ts diff --git a/lexicons/com/atproto/admin/sendEmail.json b/lexicons/com/atproto/admin/sendEmail.json new file mode 100644 index 00000000000..8df082258dd --- /dev/null +++ b/lexicons/com/atproto/admin/sendEmail.json @@ -0,0 +1,32 @@ +{ + "lexicon": 1, + "id": "com.atproto.admin.sendEmail", + "defs": { + "main": { + "type": "procedure", + "description": "Send email to a user's primary email address", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["recipientDid", "content"], + "properties": { + "recipientDid": { "type": "string", "format": "did" }, + "content": { "type": "string" }, + "subject": { "type": "string" } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["sent"], + "properties": { + "sent": { "type": "boolean" } + } + } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 9e66af0441d..7a08f0ba38d 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -22,6 +22,7 @@ import * as ComAtprotoAdminRebaseRepo from './types/com/atproto/admin/rebaseRepo import * as ComAtprotoAdminResolveModerationReports from './types/com/atproto/admin/resolveModerationReports' import * as ComAtprotoAdminReverseModerationAction from './types/com/atproto/admin/reverseModerationAction' import * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRepos' +import * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail' import * as ComAtprotoAdminTakeModerationAction from './types/com/atproto/admin/takeModerationAction' import * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail' import * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle' @@ -140,6 +141,7 @@ export * as ComAtprotoAdminRebaseRepo from './types/com/atproto/admin/rebaseRepo export * as ComAtprotoAdminResolveModerationReports from './types/com/atproto/admin/resolveModerationReports' export * as ComAtprotoAdminReverseModerationAction from './types/com/atproto/admin/reverseModerationAction' export * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRepos' +export * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail' export * as ComAtprotoAdminTakeModerationAction from './types/com/atproto/admin/takeModerationAction' export * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail' export * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle' @@ -484,6 +486,17 @@ export class AdminNS { }) } + sendEmail( + data?: ComAtprotoAdminSendEmail.InputSchema, + opts?: ComAtprotoAdminSendEmail.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.admin.sendEmail', opts?.qp, data, opts) + .catch((e) => { + throw ComAtprotoAdminSendEmail.toKnownErr(e) + }) + } + takeModerationAction( data?: ComAtprotoAdminTakeModerationAction.InputSchema, opts?: ComAtprotoAdminTakeModerationAction.CallOptions, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 3039bef31f8..e9aa2bba2ec 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -1158,6 +1158,47 @@ export const schemaDict = { }, }, }, + ComAtprotoAdminSendEmail: { + lexicon: 1, + id: 'com.atproto.admin.sendEmail', + defs: { + main: { + type: 'procedure', + description: "Send email to a user's primary email address", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['recipientDid', 'content'], + properties: { + recipientDid: { + type: 'string', + format: 'did', + }, + content: { + type: 'string', + }, + subject: { + type: 'string', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['sent'], + properties: { + sent: { + type: 'boolean', + }, + }, + }, + }, + }, + }, + }, ComAtprotoAdminTakeModerationAction: { lexicon: 1, id: 'com.atproto.admin.takeModerationAction', @@ -6397,6 +6438,7 @@ export const ids = { ComAtprotoAdminReverseModerationAction: 'com.atproto.admin.reverseModerationAction', ComAtprotoAdminSearchRepos: 'com.atproto.admin.searchRepos', + ComAtprotoAdminSendEmail: 'com.atproto.admin.sendEmail', ComAtprotoAdminTakeModerationAction: 'com.atproto.admin.takeModerationAction', ComAtprotoAdminUpdateAccountEmail: 'com.atproto.admin.updateAccountEmail', ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle', diff --git a/packages/api/src/client/types/com/atproto/admin/sendEmail.ts b/packages/api/src/client/types/com/atproto/admin/sendEmail.ts new file mode 100644 index 00000000000..d2d8b0fecbf --- /dev/null +++ b/packages/api/src/client/types/com/atproto/admin/sendEmail.ts @@ -0,0 +1,40 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface QueryParams {} + +export interface InputSchema { + recipientDid: string + content: string + subject?: string + [k: string]: unknown +} + +export interface OutputSchema { + sent: boolean + [k: string]: unknown +} + +export interface CallOptions { + headers?: Headers + qp?: QueryParams + encoding: 'application/json' +} + +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/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 982ed642924..0394591ca06 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -23,6 +23,7 @@ import * as ComAtprotoAdminRebaseRepo from './types/com/atproto/admin/rebaseRepo import * as ComAtprotoAdminResolveModerationReports from './types/com/atproto/admin/resolveModerationReports' import * as ComAtprotoAdminReverseModerationAction from './types/com/atproto/admin/reverseModerationAction' import * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRepos' +import * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail' import * as ComAtprotoAdminTakeModerationAction from './types/com/atproto/admin/takeModerationAction' import * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail' import * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle' @@ -303,6 +304,13 @@ export class AdminNS { return this._server.xrpc.method(nsid, cfg) } + sendEmail( + cfg: ConfigOf>>, + ) { + const nsid = 'com.atproto.admin.sendEmail' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + takeModerationAction( cfg: ConfigOf< AV, diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 3039bef31f8..e9aa2bba2ec 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -1158,6 +1158,47 @@ export const schemaDict = { }, }, }, + ComAtprotoAdminSendEmail: { + lexicon: 1, + id: 'com.atproto.admin.sendEmail', + defs: { + main: { + type: 'procedure', + description: "Send email to a user's primary email address", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['recipientDid', 'content'], + properties: { + recipientDid: { + type: 'string', + format: 'did', + }, + content: { + type: 'string', + }, + subject: { + type: 'string', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['sent'], + properties: { + sent: { + type: 'boolean', + }, + }, + }, + }, + }, + }, + }, ComAtprotoAdminTakeModerationAction: { lexicon: 1, id: 'com.atproto.admin.takeModerationAction', @@ -6397,6 +6438,7 @@ export const ids = { ComAtprotoAdminReverseModerationAction: 'com.atproto.admin.reverseModerationAction', ComAtprotoAdminSearchRepos: 'com.atproto.admin.searchRepos', + ComAtprotoAdminSendEmail: 'com.atproto.admin.sendEmail', ComAtprotoAdminTakeModerationAction: 'com.atproto.admin.takeModerationAction', ComAtprotoAdminUpdateAccountEmail: 'com.atproto.admin.updateAccountEmail', ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle', diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/sendEmail.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/sendEmail.ts new file mode 100644 index 00000000000..356d4513d1a --- /dev/null +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/sendEmail.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 { + recipientDid: string + content: string + subject?: 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 Handler = (ctx: { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +}) => Promise | HandlerOutput diff --git a/packages/pds/src/api/com/atproto/admin/index.ts b/packages/pds/src/api/com/atproto/admin/index.ts index e50e2324e46..78524abdb10 100644 --- a/packages/pds/src/api/com/atproto/admin/index.ts +++ b/packages/pds/src/api/com/atproto/admin/index.ts @@ -17,6 +17,7 @@ import getInviteCodes from './getInviteCodes' import updateAccountHandle from './updateAccountHandle' import updateAccountEmail from './updateAccountEmail' import rebaseRepo from './rebaseRepo' +import sendEmail from './sendEmail' export default function (server: Server, ctx: AppContext) { resolveModerationReports(server, ctx) @@ -36,4 +37,5 @@ export default function (server: Server, ctx: AppContext) { updateAccountHandle(server, ctx) updateAccountEmail(server, ctx) rebaseRepo(server, ctx) + sendEmail(server, ctx) } diff --git a/packages/pds/src/api/com/atproto/admin/sendEmail.ts b/packages/pds/src/api/com/atproto/admin/sendEmail.ts new file mode 100644 index 00000000000..2d8c400ff95 --- /dev/null +++ b/packages/pds/src/api/com/atproto/admin/sendEmail.ts @@ -0,0 +1,38 @@ +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.admin.sendEmail({ + auth: ctx.roleVerifier, + handler: async ({ input, auth }) => { + if (!auth.credentials.admin && !auth.credentials.moderator) { + throw new AuthRequiredError('Insufficient privileges') + } + + const { + content, + recipientDid, + subject = 'Message from Bluesky moderator', + } = input.body + const userInfo = await ctx.db.db + .selectFrom('user_account') + .where('did', '=', recipientDid) + .select('email') + .executeTakeFirst() + + if (!userInfo) { + throw new InvalidRequestError('Recipient not found') + } + + await ctx.moderationMailer.send( + { content }, + { subject, to: userInfo.email }, + ) + return { + encoding: 'application/json', + body: { sent: true }, + } + }, + }) +} diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index 4c15fa64b83..fdfb1e3677f 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -46,6 +46,8 @@ export interface ServerConfigValues { appUrlPasswordReset: string emailSmtpUrl?: string emailNoReplyAddress: string + moderationEmailAddress?: string + moderationEmailSmtpUrl?: string hiveApiKey?: string labelerDid: string @@ -162,6 +164,11 @@ export class ServerConfig { const emailNoReplyAddress = process.env.EMAIL_NO_REPLY_ADDRESS || 'noreply@blueskyweb.xyz' + const moderationEmailAddress = + process.env.MODERATION_EMAIL_ADDRESS || undefined + const moderationEmailSmtpUrl = + process.env.MODERATION_EMAIL_SMTP_URL || undefined + const hiveApiKey = process.env.HIVE_API_KEY || undefined const labelerDid = process.env.LABELER_DID || 'did:example:labeler' const labelerKeywords = {} @@ -247,6 +254,8 @@ export class ServerConfig { appUrlPasswordReset, emailSmtpUrl, emailNoReplyAddress, + moderationEmailAddress, + moderationEmailSmtpUrl, hiveApiKey, labelerDid, labelerKeywords, @@ -432,6 +441,14 @@ export class ServerConfig { return this.cfg.emailNoReplyAddress } + get moderationEmailAddress() { + return this.cfg.moderationEmailAddress + } + + get moderationEmailSmtpUrl() { + return this.cfg.moderationEmailSmtpUrl + } + get hiveApiKey() { return this.cfg.hiveApiKey } diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 4aa74e45156..d315e10f1cc 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -8,6 +8,7 @@ import { Database } from './db' import { ServerConfig } from './config' import * as auth from './auth' import { ServerMailer } from './mailer' +import { ModerationMailer } from './mailer/moderation' import { BlobStore } from '@atproto/repo' import { ImageUriBuilder } from './image/uri' import { Services } from './services' @@ -36,6 +37,7 @@ export class AppContext { imgUriBuilder: ImageUriBuilder cfg: ServerConfig mailer: ServerMailer + moderationMailer: ModerationMailer services: Services messageDispatcher: MessageDispatcher sequencer: Sequencer @@ -111,6 +113,10 @@ export class AppContext { return this.opts.mailer } + get moderationMailer(): ModerationMailer { + return this.opts.moderationMailer + } + get services(): Services { return this.opts.services } diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 33a93eb35f5..4b4511b4cd6 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -24,6 +24,7 @@ import compression from './util/compression' import { dbLogger, loggerMiddleware, seqLogger } from './logger' import { ServerConfig } from './config' import { ServerMailer } from './mailer' +import { ModerationMailer } from './mailer/moderation' import { createServer } from './lexicon' import { MessageDispatcher } from './event-stream/message-queue' import { ImageUriBuilder } from './image/uri' @@ -108,12 +109,21 @@ export class PDS { ? new SequencerLeader(db, config.sequencerLeaderLockId) : null - const mailTransport = + const serverMailTransport = config.emailSmtpUrl !== undefined ? createTransport(config.emailSmtpUrl) : createTransport({ jsonTransport: true }) - const mailer = new ServerMailer(mailTransport, config) + const moderationMailTransport = + config.moderationEmailSmtpUrl !== undefined + ? createTransport(config.moderationEmailSmtpUrl) + : createTransport({ jsonTransport: true }) + + const mailer = new ServerMailer(serverMailTransport, config) + const moderationMailer = new ModerationMailer( + moderationMailTransport, + config, + ) const app = express() app.use(cors()) @@ -224,6 +234,7 @@ export class PDS { contentReporter, services, mailer, + moderationMailer, imgUriBuilder, backgroundQueue, crawlers, diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 982ed642924..0394591ca06 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -23,6 +23,7 @@ import * as ComAtprotoAdminRebaseRepo from './types/com/atproto/admin/rebaseRepo import * as ComAtprotoAdminResolveModerationReports from './types/com/atproto/admin/resolveModerationReports' import * as ComAtprotoAdminReverseModerationAction from './types/com/atproto/admin/reverseModerationAction' import * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRepos' +import * as ComAtprotoAdminSendEmail from './types/com/atproto/admin/sendEmail' import * as ComAtprotoAdminTakeModerationAction from './types/com/atproto/admin/takeModerationAction' import * as ComAtprotoAdminUpdateAccountEmail from './types/com/atproto/admin/updateAccountEmail' import * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle' @@ -303,6 +304,13 @@ export class AdminNS { return this._server.xrpc.method(nsid, cfg) } + sendEmail( + cfg: ConfigOf>>, + ) { + const nsid = 'com.atproto.admin.sendEmail' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + takeModerationAction( cfg: ConfigOf< AV, diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 3039bef31f8..e9aa2bba2ec 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -1158,6 +1158,47 @@ export const schemaDict = { }, }, }, + ComAtprotoAdminSendEmail: { + lexicon: 1, + id: 'com.atproto.admin.sendEmail', + defs: { + main: { + type: 'procedure', + description: "Send email to a user's primary email address", + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['recipientDid', 'content'], + properties: { + recipientDid: { + type: 'string', + format: 'did', + }, + content: { + type: 'string', + }, + subject: { + type: 'string', + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['sent'], + properties: { + sent: { + type: 'boolean', + }, + }, + }, + }, + }, + }, + }, ComAtprotoAdminTakeModerationAction: { lexicon: 1, id: 'com.atproto.admin.takeModerationAction', @@ -6397,6 +6438,7 @@ export const ids = { ComAtprotoAdminReverseModerationAction: 'com.atproto.admin.reverseModerationAction', ComAtprotoAdminSearchRepos: 'com.atproto.admin.searchRepos', + ComAtprotoAdminSendEmail: 'com.atproto.admin.sendEmail', ComAtprotoAdminTakeModerationAction: 'com.atproto.admin.takeModerationAction', ComAtprotoAdminUpdateAccountEmail: 'com.atproto.admin.updateAccountEmail', ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle', diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/sendEmail.ts b/packages/pds/src/lexicon/types/com/atproto/admin/sendEmail.ts new file mode 100644 index 00000000000..356d4513d1a --- /dev/null +++ b/packages/pds/src/lexicon/types/com/atproto/admin/sendEmail.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 { + recipientDid: string + content: string + subject?: 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 Handler = (ctx: { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +}) => Promise | HandlerOutput diff --git a/packages/pds/src/mailer/moderation.ts b/packages/pds/src/mailer/moderation.ts new file mode 100644 index 00000000000..6d77d7853c9 --- /dev/null +++ b/packages/pds/src/mailer/moderation.ts @@ -0,0 +1,34 @@ +import { Transporter } from 'nodemailer' +import Mail from 'nodemailer/lib/mailer' +import SMTPTransport from 'nodemailer/lib/smtp-transport' +import { ServerConfig } from '../config' +import { mailerLogger } from '../logger' + +export class ModerationMailer { + private config: ServerConfig + transporter: Transporter + + constructor( + transporter: Transporter, + config: ServerConfig, + ) { + this.config = config + this.transporter = transporter + } + + async send({ content }: { content: string }, mailOpts: Mail.Options) { + const res = await this.transporter.sendMail({ + ...mailOpts, + text: content, + from: this.config.moderationEmailAddress, + }) + + if (!this.config.moderationEmailSmtpUrl) { + mailerLogger.debug( + 'Moderation email auth is not configured. Intended to send email:\n' + + JSON.stringify(res, null, 2), + ) + } + return res + } +} From 754ad7f4f9a53d090736f7bfb91bbdc9c1a6d1dc Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Mon, 17 Jul 2023 19:10:42 -0400 Subject: [PATCH 048/237] v0.4.2 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 652a971f845..bdc201933d2 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.4.1", + "version": "0.4.2", "main": "src/index.ts", "scripts": { "codegen": "lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From bb607ccc55c9a87b7f6de6ad74c9fda3e74a6aea Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 18 Jul 2023 13:17:20 -0500 Subject: [PATCH 049/237] Patch up a couple sqlite tests (#1355) patch up a couple sqlite tests --- packages/pds/src/sequencer/events.ts | 1 + packages/pds/src/sequencer/sequencer-leader.ts | 1 + packages/pds/tests/algos/whats-hot.test.ts | 4 ++++ packages/pds/tests/sequencer.test.ts | 2 +- 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/pds/src/sequencer/events.ts b/packages/pds/src/sequencer/events.ts index 75b5263e700..fc6e98729cd 100644 --- a/packages/pds/src/sequencer/events.ts +++ b/packages/pds/src/sequencer/events.ts @@ -35,6 +35,7 @@ export const sequenceEvt = async (dbTxn: Database, evt: RepoSeqInsert) => { .set({ seq: res.id }) .where('id', '=', res.id) .execute() + await dbTxn.notify('outgoing_repo_seq') } } diff --git a/packages/pds/src/sequencer/sequencer-leader.ts b/packages/pds/src/sequencer/sequencer-leader.ts index 32964243f2e..fab3a3b4602 100644 --- a/packages/pds/src/sequencer/sequencer-leader.ts +++ b/packages/pds/src/sequencer/sequencer-leader.ts @@ -140,6 +140,7 @@ export class SequencerLeader { } async isCaughtUp(): Promise { + if (this.db.dialect === 'sqlite') return true const unsequenced = await this.getUnsequenced() return unsequenced.length === 0 } diff --git a/packages/pds/tests/algos/whats-hot.test.ts b/packages/pds/tests/algos/whats-hot.test.ts index 08d9f1a1b82..0b0a292c530 100644 --- a/packages/pds/tests/algos/whats-hot.test.ts +++ b/packages/pds/tests/algos/whats-hot.test.ts @@ -46,6 +46,8 @@ describe('algo whats-hot', () => { }) it('returns well liked posts', async () => { + if (server.ctx.db.dialect === 'sqlite') return + const img = await sc.uploadFile( alice, 'tests/image/fixtures/key-landscape-small.jpg', @@ -101,6 +103,8 @@ describe('algo whats-hot', () => { }) it('paginates', async () => { + if (server.ctx.db.dialect === 'sqlite') return + const res = await agent.api.app.bsky.feed.getFeed( { feed: feedUri }, { headers: sc.getHeaders(alice) }, diff --git a/packages/pds/tests/sequencer.test.ts b/packages/pds/tests/sequencer.test.ts index dab71fba74d..c41cedafb58 100644 --- a/packages/pds/tests/sequencer.test.ts +++ b/packages/pds/tests/sequencer.test.ts @@ -81,7 +81,7 @@ describe('sequencer', () => { const caughtUp = (outbox: Outbox): (() => Promise) => { return async () => { - const leaderCaughtUp = await server.ctx.sequencerLeader.isCaughtUp() + const leaderCaughtUp = await server.ctx.sequencerLeader?.isCaughtUp() if (!leaderCaughtUp) return false const lastEvt = await outbox.sequencer.curr() if (!lastEvt) return true From 8c32ed363bd9c761454e14216b4e87f4ebc90b6e Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 19 Jul 2023 09:03:01 -0500 Subject: [PATCH 050/237] Increase appview keepalive (#1360) increase keepalive to 90s --- packages/bsky/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 8af95aba95c..537c30e4d80 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -204,6 +204,7 @@ export class BskyAppView { } const server = this.app.listen(this.ctx.cfg.port) this.server = server + server.keepAliveTimeout = 90000 this.terminator = createHttpTerminator({ server }) await events.once(server, 'listening') const { port } = server.address() as AddressInfo From 64137c7e5c3fdeb043b23b567595db86e7312bff Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 19 Jul 2023 17:22:11 -0500 Subject: [PATCH 051/237] add optional ilike query to getPopularFeedGenerators --- .../app/bsky/unspecced/getPopularFeedGenerators.json | 3 ++- packages/api/src/client/lexicons.ts | 5 ++++- .../app/bsky/unspecced/getPopularFeedGenerators.ts | 1 + .../app/bsky/unspecced/getPopularFeedGenerators.ts | 11 +++++++++-- packages/bsky/src/lexicon/lexicons.ts | 5 ++++- .../app/bsky/unspecced/getPopularFeedGenerators.ts | 1 + packages/pds/src/app-view/api/app/bsky/unspecced.ts | 11 +++++++++-- packages/pds/src/lexicon/lexicons.ts | 5 ++++- .../app/bsky/unspecced/getPopularFeedGenerators.ts | 1 + packages/pds/tests/feed-generation.test.ts | 10 ++++++++++ 10 files changed, 45 insertions(+), 8 deletions(-) diff --git a/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json b/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json index 5b1bce07cc8..55a85bcb51d 100644 --- a/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json +++ b/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json @@ -9,7 +9,8 @@ "type": "params", "properties": { "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "cursor": {"type": "string"}, + "query": {"type": "string"} } }, "output": { diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 68186fdb49e..33dc0900f6c 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -6297,6 +6297,9 @@ export const schemaDict = { cursor: { type: 'string', }, + query: { + type: 'string', + }, }, }, output: { @@ -6327,7 +6330,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'A skeleton of a timeline', + description: 'A skeleton of a timeline - UNSPECCED & WILL GO AWAY SOON', parameters: { type: 'params', properties: { diff --git a/packages/api/src/client/types/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/api/src/client/types/app/bsky/unspecced/getPopularFeedGenerators.ts index d4764648085..19ca5cbc597 100644 --- a/packages/api/src/client/types/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/api/src/client/types/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -11,6 +11,7 @@ import * as AppBskyFeedDefs from '../feed/defs' export interface QueryParams { limit?: number cursor?: string + query?: string } export type InputSchema = undefined diff --git a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index dff5965f930..0b716b3b3ca 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -10,13 +10,13 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.unspecced.getPopularFeedGenerators({ auth: ctx.authOptionalVerifier, handler: async ({ auth, params }) => { - const { limit, cursor } = params + const { limit, cursor, query } = params const requester = auth.credentials.did const db = ctx.db.db const { ref } = db.dynamic const feedService = ctx.services.feed(ctx.db) - const inner = ctx.db.db + let inner = ctx.db.db .selectFrom('feed_generator') .select([ 'uri', @@ -28,6 +28,13 @@ export default function (server: Server, ctx: AppContext) { .as('likeCount'), ]) + if (query) { + inner = inner.where(qb => qb + .where('feed_generator.displayName', 'ilike', `%${query}%`) + .orWhere('feed_generator.description', 'ilike', `%${query}%`) + ) + } + let builder = ctx.db.db.selectFrom(inner.as('feed_gens')).selectAll() const keyset = new LikeCountKeyset(ref('likeCount'), ref('cid')) diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 68186fdb49e..33dc0900f6c 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -6297,6 +6297,9 @@ export const schemaDict = { cursor: { type: 'string', }, + query: { + type: 'string', + }, }, }, output: { @@ -6327,7 +6330,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'A skeleton of a timeline', + description: 'A skeleton of a timeline - UNSPECCED & WILL GO AWAY SOON', parameters: { type: 'params', properties: { diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts index 0b53114fe94..58f385b958e 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -12,6 +12,7 @@ import * as AppBskyFeedDefs from '../feed/defs' export interface QueryParams { limit: number cursor?: string + query?: string } export type InputSchema = undefined diff --git a/packages/pds/src/app-view/api/app/bsky/unspecced.ts b/packages/pds/src/app-view/api/app/bsky/unspecced.ts index 56129f9e904..3b654e9aeac 100644 --- a/packages/pds/src/app-view/api/app/bsky/unspecced.ts +++ b/packages/pds/src/app-view/api/app/bsky/unspecced.ts @@ -131,11 +131,11 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ auth, params }) => { const requester = auth.credentials.did const db = ctx.db.db - const { limit, cursor } = params + const { limit, cursor, query } = params const { ref } = db.dynamic const feedService = ctx.services.appView.feed(ctx.db) - const inner = ctx.db.db + let inner = ctx.db.db .selectFrom('feed_generator') .select([ 'uri', @@ -147,6 +147,13 @@ export default function (server: Server, ctx: AppContext) { .as('likeCount'), ]) + if (query) { + inner = inner.where(qb => qb + .where('feed_generator.displayName', 'ilike', `%${query}%`) + .orWhere('feed_generator.description', 'ilike', `%${query}%`) + ) + } + let builder = ctx.db.db.selectFrom(inner.as('feed_gens')).selectAll() const keyset = new LikeCountKeyset(ref('likeCount'), ref('cid')) diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 68186fdb49e..33dc0900f6c 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -6297,6 +6297,9 @@ export const schemaDict = { cursor: { type: 'string', }, + query: { + type: 'string', + }, }, }, output: { @@ -6327,7 +6330,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'A skeleton of a timeline', + description: 'A skeleton of a timeline - UNSPECCED & WILL GO AWAY SOON', parameters: { type: 'params', properties: { diff --git a/packages/pds/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/pds/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts index 0b53114fe94..58f385b958e 100644 --- a/packages/pds/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/pds/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -12,6 +12,7 @@ import * as AppBskyFeedDefs from '../feed/defs' export interface QueryParams { limit: number cursor?: string + query?: string } export type InputSchema = undefined diff --git a/packages/pds/tests/feed-generation.test.ts b/packages/pds/tests/feed-generation.test.ts index b8891ae0ef2..538c592079f 100644 --- a/packages/pds/tests/feed-generation.test.ts +++ b/packages/pds/tests/feed-generation.test.ts @@ -336,6 +336,16 @@ describe('feed generation', () => { resFull.data.feeds, ) }) + + it('searches', async () => { + const res = + await agent.api.app.bsky.unspecced.getPopularFeedGenerators( + { query: 'pagination' }, + { headers: sc.getHeaders(sc.dids.bob) }, + ) + + expect(res.data.feeds[0].displayName).toBe('Bad Pagination') + }) }) describe('getFeed', () => { From 77274b31a04e07920ad2886260a705799d441b2b Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 19 Jul 2023 17:37:06 -0500 Subject: [PATCH 052/237] fix lint --- packages/pds/src/app-view/api/app/bsky/unspecced.ts | 7 ++++--- packages/pds/tests/feed-generation.test.ts | 9 ++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/pds/src/app-view/api/app/bsky/unspecced.ts b/packages/pds/src/app-view/api/app/bsky/unspecced.ts index 3b654e9aeac..e9610d78291 100644 --- a/packages/pds/src/app-view/api/app/bsky/unspecced.ts +++ b/packages/pds/src/app-view/api/app/bsky/unspecced.ts @@ -148,9 +148,10 @@ export default function (server: Server, ctx: AppContext) { ]) if (query) { - inner = inner.where(qb => qb - .where('feed_generator.displayName', 'ilike', `%${query}%`) - .orWhere('feed_generator.description', 'ilike', `%${query}%`) + inner = inner.where((qb) => + qb + .where('feed_generator.displayName', 'ilike', `%${query}%`) + .orWhere('feed_generator.description', 'ilike', `%${query}%`), ) } diff --git a/packages/pds/tests/feed-generation.test.ts b/packages/pds/tests/feed-generation.test.ts index 538c592079f..41b21901ba4 100644 --- a/packages/pds/tests/feed-generation.test.ts +++ b/packages/pds/tests/feed-generation.test.ts @@ -338,11 +338,10 @@ describe('feed generation', () => { }) it('searches', async () => { - const res = - await agent.api.app.bsky.unspecced.getPopularFeedGenerators( - { query: 'pagination' }, - { headers: sc.getHeaders(sc.dids.bob) }, - ) + const res = await agent.api.app.bsky.unspecced.getPopularFeedGenerators( + { query: 'pagination' }, + { headers: sc.getHeaders(sc.dids.bob) }, + ) expect(res.data.feeds[0].displayName).toBe('Bad Pagination') }) From 2836ee38bb1ae4b64ee324f42938a4b62c1b2412 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 19 Jul 2023 17:38:43 -0500 Subject: [PATCH 053/237] Appview - serve feed skeletons (#1265) * proxy timeline skeleton construction to appview * add getFeedSkeleton to appview * mount route * smart proxy feed skeletons * tests * proper error code * only proxy specific feeds * build branch * update proxyable feed logic, should use feed publisher rather than generator * fix feed proxy tests, configure feed publisher (in addition to generator) * hotfix: prevent user-supplied rkey on posts with createRecord (#1313) * prevent user-supplied rkey on posts with createRecord * allow empty-string rkey parameter Co-authored-by: devin ivy --------- Co-authored-by: devin ivy * add slurs to reserved words (#1318) * add slurs to reserved words (#1314) * Update reserved.ts Add slurs to reserved words * Update reserved.ts fix typo * Update reserved.ts to clean up the slur list * linting * pluralise --------- Co-authored-by: jess * identifier: tweaks and additions to slur list (#1319) * Refactor appview repo subscription for memleak (#1308) * refactor to remove closure in loop * move consecutive item out of p-queue * Handle validation improvements (#1336) * Handle matches and false positives for unacceptable words in handles * move handle validation logic to pds * missed merge * add cfg flag * encode lists * fix build issues * move words to cfg * tidy --------- Co-authored-by: Jaz Volpert * Allow moderators to take and reverse actor takedowns (#1330) allow moderators to take and reverse actor takedowns * :sparkles: Allow searching reports by moderator did (#1283) * :sparkles: Allow searching reports by moderator did * :white_check_mark: Remove .only flags on tests * :white_check_mark: Update snapshot * :white_check_mark: Add checks for did match in actions * v0.4.1 * Make sequencer leader behavior optional on pds (#1250) * make sequencer leader behavior optional on pds * tidy * use 127.0.0.1 in with-test-db.sh for colima (#1297) So, since Docker Desktop has licensing issues, some folks use colima for running containers on their macOS machines (The licensing exempted CLI-only version of Docker only exists on Linux). Unfortunately, colima binds host ports only on the IPv4 localhost address (`127.0.0.1`) while the atproto postgres clients will attempt to connect to the IPv6 localhost address (`::1`) that macOS sets in /etc/hosts. See https://github.com/abiosoft/colima/issues/583 and https://github.com/lima-vm/lima/issues/1330 for the tickets against colima. (Docker Desktop binds to both IPv4 and IPv6 localhost addresses and so doesn't have this issue.) To workaround this silly issue, we can use `localhost` within the docker containers and docker-compose, but need to set the `DB_POSTGRES_URL` env var to use the IPv4 localhost explicitly. (Asking folks to edit /etc/hosts causes other tools to break and will be overridden on each OS upgrade.) * Subscription util tests (#1295) * consecutive list tests * flesh out subscription util tests --------- Co-authored-by: Devin Ivy * Content reporting on record fields (#1351) * content reporting on record fields * fix test * tests * tidy * Check rkey contents just for non-tids (#1353) check rkey content for non-tids * :sparkles: Added new procedure for sending admin email (#1312) * :construction: Added new lexicon for sending admin email * :sparkles: Add moderation mailer * :sparkles: Switch to text email content from html * :broom: Cleanup some early implementation code and reflect PR reivew * :sparkles: Use smtp host instead of gmail service config * :sparkles: Move to using single smtp url * v0.4.2 * Patch up a couple sqlite tests (#1355) patch up a couple sqlite tests * enable feeds & build branch * disable branch building & enable without proxy header --------- Co-authored-by: Devin Ivy Co-authored-by: David Buchanan Co-authored-by: jess Co-authored-by: bnewbold Co-authored-by: Jaz Volpert Co-authored-by: Foysal Ahamed Co-authored-by: Jeff Hodges --- .../src/api/app/bsky/feed/getFeedSkeleton.ts | 39 + packages/bsky/src/api/index.ts | 2 + packages/dev-env/src/pds.ts | 1 + packages/dev-env/src/types.ts | 1 + .../src/app-view/api/app/bsky/feed/getFeed.ts | 50 +- packages/pds/src/index.ts | 1 + .../timeline-skeleton.test.ts.snap | 788 ++++++++++++++++++ .../tests/proxied/timeline-skeleton.test.ts | 41 +- 8 files changed, 913 insertions(+), 10 deletions(-) create mode 100644 packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts b/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts new file mode 100644 index 00000000000..9f808b35726 --- /dev/null +++ b/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts @@ -0,0 +1,39 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.feed.getFeedSkeleton({ + auth: ctx.authVerifierAnyAudience, + handler: async ({ params, auth }) => { + const { feed } = params + const viewer = auth.credentials.did + const localAlgo = ctx.algos[feed] + + if (!localAlgo) { + throw new InvalidRequestError('Unknown feed', 'UnknownFeed') + } + + const { cursor, feedItems } = await localAlgo(ctx, params, viewer) + + const skeleton = feedItems.map((item) => ({ + post: item.postUri, + reason: + item.uri === item.postUri + ? undefined + : { + $type: 'app.bsky.feed.defs#skeletonReasonRepost', + repost: item.uri, + }, + })) + + return { + encoding: 'application/json', + body: { + cursor, + feed: skeleton, + }, + } + }, + }) +} diff --git a/packages/bsky/src/api/index.ts b/packages/bsky/src/api/index.ts index 691eae882d1..78edf7c51b4 100644 --- a/packages/bsky/src/api/index.ts +++ b/packages/bsky/src/api/index.ts @@ -7,6 +7,7 @@ import getAuthorFeed from './app/bsky/feed/getAuthorFeed' import getFeed from './app/bsky/feed/getFeed' import getFeedGenerator from './app/bsky/feed/getFeedGenerator' import getFeedGenerators from './app/bsky/feed/getFeedGenerators' +import getFeedSkeleton from './app/bsky/feed/getFeedSkeleton' import getLikes from './app/bsky/feed/getLikes' import getPostThread from './app/bsky/feed/getPostThread' import getPosts from './app/bsky/feed/getPosts' @@ -59,6 +60,7 @@ export default function (server: Server, ctx: AppContext) { getFeed(server, ctx) getFeedGenerator(server, ctx) getFeedGenerators(server, ctx) + getFeedSkeleton(server, ctx) getLikes(server, ctx) getPostThread(server, ctx) getPosts(server, ctx) diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 7c665304d58..88174d0c9a9 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -95,6 +95,7 @@ export class TestPds { repoSigningKey, plcRotationKey, config, + algos: cfg.algos, }) await server.start() diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index 829d7f7518e..927a3364ccc 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -10,6 +10,7 @@ export type PdsConfig = Partial & { plcUrl: string migration?: string enableInProcessAppView?: boolean + algos?: pds.MountedAlgos } export type BskyConfig = Partial & { diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts index ca5c09ce39e..d1431342a08 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts @@ -11,7 +11,7 @@ import { PoorlyFormattedDidDocumentError, getFeedGen, } from '@atproto/identity' -import { AtpAgent, AppBskyFeedGetFeedSkeleton } from '@atproto/api' +import { AtpAgent, AppBskyFeedGetFeedSkeleton, AtUri } from '@atproto/api' import { SkeletonFeedPost } from '../../../../../lexicon/types/app/bsky/feed/defs' import { QueryParams as GetFeedParams } from '../../../../../lexicon/types/app/bsky/feed/getFeed' import { OutputSchema as SkeletonOutput } from '../../../../../lexicon/types/app/bsky/feed/getFeedSkeleton' @@ -20,11 +20,26 @@ import AppContext from '../../../../../context' import { FeedRow } from '../../../../services/feed' import { AlgoResponse } from '../../../../../feed-gen/types' +// temp hardcoded feeds that we can proxy to appview +const PROXYABLE_FEEDS = [ + 'with-friends', + 'bsky-team', + 'hot-classic', + 'best-of-follows', + 'mutuals', +] + export default function (server: Server, ctx: AppContext) { + const isProxyableFeed = (feed: string): boolean => { + const uri = new AtUri(feed) + return feed in ctx.algos && PROXYABLE_FEEDS.includes(uri.rkey) + } + server.app.bsky.feed.getFeed({ auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did + if (ctx.canProxyRead(req)) { const { data: feed } = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( @@ -40,18 +55,35 @@ export default function (server: Server, ctx: AppContext) { body: res.data, } } + let algoRes: AlgoResponse + const timerSkele = new ServerTimer('skele').start() - const { feed } = params - const feedService = ctx.services.appView.feed(ctx.db) - const localAlgo = ctx.algos[feed] + if (ctx.cfg.bskyAppViewEndpoint && isProxyableFeed(params.feed)) { + // this is a temporary solution to smart proxy bsky feeds to the appview + const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedSkeleton( + params, + await ctx.serviceAuthHeaders(requester), + ) + algoRes = await filterMutesAndBlocks( + ctx, + res.data, + params.limit, + requester, + ) + } else { + const { feed } = params + const localAlgo = ctx.algos[feed] + algoRes = + localAlgo !== undefined + ? await localAlgo(ctx, params, requester) + : await skeletonFromFeedGen(ctx, params, requester) + } - const timerSkele = new ServerTimer('skele').start() - const { feedItems, ...rest } = - localAlgo !== undefined - ? await localAlgo(ctx, params, requester) - : await skeletonFromFeedGen(ctx, params, requester) timerSkele.stop() + const feedService = ctx.services.appView.feed(ctx.db) + const { feedItems, ...rest } = algoRes + const timerHydr = new ServerTimer('hydr').start() const hydrated = await feedService.hydrateFeed(feedItems, requester) timerHydr.stop() diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 4b4511b4cd6..e2c4187ae6a 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -46,6 +46,7 @@ import { LabelCache } from './label-cache' import { ContentReporter } from './content-reporter' import { ModerationService } from './services/moderation' +export type { MountedAlgos } from './feed-gen/types' export type { ServerConfigValues } from './config' export { ServerConfig } from './config' export { Database } from './db' diff --git a/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap index 3c6ca0ad290..15229efe6cd 100644 --- a/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap @@ -1,5 +1,793 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`proxies timeline skeleton feed skeleton construction 1`] = ` +Object { + "cursor": "0000000000000::bafycid", + "feed": Array [ + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(2)", + "uri": "record(2)", + }, + "root": Object { + "cid": "cids(1)", + "uri": "record(1)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(0)", + "viewer": Object {}, + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(1)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(4)", + "following": "record(3)", + "muted": false, + }, + }, + "cid": "cids(2)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(2)", + "val": "test-label", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(2)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(3)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(1)", + "uri": "record(1)", + }, + "root": Object { + "cid": "cids(1)", + "uri": "record(1)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(2)", + "viewer": Object {}, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(1)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(1)", + "viewer": Object {}, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(2)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(7)", + "following": "record(6)", + "muted": false, + }, + }, + "cid": "cids(4)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(1)", + "uri": "record(1)", + }, + "root": Object { + "cid": "cids(1)", + "uri": "record(1)", + }, + }, + "text": "of course", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(5)", + "viewer": Object {}, + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(1)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(1)", + "viewer": Object {}, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(1)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(1)", + "viewer": Object {}, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(1)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(4)", + "following": "record(3)", + "muted": false, + }, + }, + "cid": "cids(2)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(2)", + "val": "test-label", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(2)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(3)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(1)", + "uri": "record(1)", + }, + "root": Object { + "cid": "cids(1)", + "uri": "record(1)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(2)", + "viewer": Object {}, + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(1)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(1)", + "viewer": Object {}, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(1)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(1)", + "viewer": Object {}, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(5)", + "embed": Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "did": "user(3)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(3)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(10)", + "muted": false, + }, + }, + "cid": "cids(6)", + "embeds": Array [ + Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "did": "user(2)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(7)", + "following": "record(6)", + "muted": false, + }, + }, + "cid": "cids(7)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(11)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia", + "media": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(3)", + }, + "size": 4114, + }, + }, + Object { + "alt": "tests/image/fixtures/key-alt.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(8)", + }, + "size": 12736, + }, + }, + ], + }, + "record": Object { + "record": Object { + "cid": "cids(9)", + "uri": "record(12)", + }, + }, + }, + "text": "hi im carol", + }, + }, + }, + ], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(9)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(7)", + "uri": "record(11)", + }, + }, + "facets": Array [ + Object { + "features": Array [ + Object { + "$type": "app.bsky.richtext.facet#mention", + "did": "user(0)", + }, + ], + "index": Object { + "byteEnd": 18, + "byteStart": 0, + }, + }, + ], + "text": "@alice.bluesky.xyz is the best", + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(8)", + "val": "test-label", + }, + ], + "likeCount": 2, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(6)", + "uri": "record(9)", + }, + }, + "text": "yoohoo label_me", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(8)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(1)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(4)", + "following": "record(3)", + "muted": false, + }, + }, + "cid": "cids(10)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000+00:00", + "text": "bobby boy here", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(13)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(1)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(1)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(2)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(7)", + "following": "record(6)", + "muted": false, + }, + }, + "cid": "cids(7)", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia#view", + "media": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + }, + Object { + "alt": "tests/image/fixtures/key-alt.jpg", + "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", + "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", + }, + ], + }, + "record": Object { + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(1)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(4)", + "following": "record(3)", + "muted": false, + }, + }, + "cid": "cids(9)", + "embeds": Array [], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(12)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "langs": Array [ + "en-US", + "i-klingon", + ], + "text": "bob back at it again!", + }, + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 2, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.recordWithMedia", + "media": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(3)", + }, + "size": 4114, + }, + }, + Object { + "alt": "tests/image/fixtures/key-alt.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(8)", + }, + "size": 12736, + }, + }, + ], + }, + "record": Object { + "record": Object { + "cid": "cids(9)", + "uri": "record(12)", + }, + }, + }, + "text": "hi im carol", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(11)", + "viewer": Object { + "like": "record(14)", + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(1)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(4)", + "following": "record(3)", + "muted": false, + }, + }, + "cid": "cids(9)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "langs": Array [ + "en-US", + "i-klingon", + ], + "text": "bob back at it again!", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(12)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(11)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "hey there", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(15)", + "viewer": Object {}, + }, + }, + ], +} +`; + exports[`proxies timeline skeleton timeline skeleton construction 1`] = ` Object { "cursor": "0000000000000::bafycid", diff --git a/packages/pds/tests/proxied/timeline-skeleton.test.ts b/packages/pds/tests/proxied/timeline-skeleton.test.ts index 2131150e3a3..d7617a74a16 100644 --- a/packages/pds/tests/proxied/timeline-skeleton.test.ts +++ b/packages/pds/tests/proxied/timeline-skeleton.test.ts @@ -1,8 +1,10 @@ -import AtpAgent from '@atproto/api' +import AtpAgent, { AtUri } from '@atproto/api' import { TestNetwork } from '@atproto/dev-env' import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' import { forSnapshot } from '../_util' +import * as bsky from '@atproto/bsky' +import { makeAlgos } from '../../src' describe('proxies timeline skeleton', () => { let network: TestNetwork @@ -11,13 +13,22 @@ describe('proxies timeline skeleton', () => { let alice: string + const feedGenDid = 'did:example:feed-gen' + const feedPublisherDid = 'did:example:feed-publisher' + beforeAll(async () => { network = await TestNetwork.create({ dbPostgresSchema: 'proxy_timeline_skeleton', pds: { + feedGenDid, + algos: makeAlgos(feedPublisherDid), enableInProcessAppView: true, bskyAppViewProxy: false, }, + bsky: { + feedGenDid, + algos: bsky.makeAlgos(feedPublisherDid), + }, }) agent = network.pds.getClient() sc = new SeedClient(agent) @@ -57,4 +68,32 @@ describe('proxies timeline skeleton', () => { ) expect([...pt1.data.feed, ...pt2.data.feed]).toEqual(res.data.feed) }) + + it('feed skeleton construction', async () => { + const uri = AtUri.make( + feedPublisherDid, + 'app.bsky.feed.generator', + 'mutuals', + ) + const res = await agent.api.app.bsky.feed.getFeed( + { feed: uri.toString() }, + { + headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, + }, + ) + expect(forSnapshot(res.data)).toMatchSnapshot() + const pt1 = await agent.api.app.bsky.feed.getFeed( + { feed: uri.toString(), limit: 2 }, + { + headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, + }, + ) + const pt2 = await agent.api.app.bsky.feed.getFeed( + { feed: uri.toString(), cursor: pt1.data.cursor }, + { + headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' }, + }, + ) + expect([...pt1.data.feed, ...pt2.data.feed]).toEqual(res.data.feed) + }) }) From 0b33d30140a6bfc2cad027689beec8c5c31d54d9 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 19 Jul 2023 17:47:32 -0500 Subject: [PATCH 054/237] fix lint --- .../src/api/app/bsky/unspecced/getPopularFeedGenerators.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index 0b716b3b3ca..2f665f06429 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -29,9 +29,10 @@ export default function (server: Server, ctx: AppContext) { ]) if (query) { - inner = inner.where(qb => qb - .where('feed_generator.displayName', 'ilike', `%${query}%`) - .orWhere('feed_generator.description', 'ilike', `%${query}%`) + inner = inner.where((qb) => + qb + .where('feed_generator.displayName', 'ilike', `%${query}%`) + .orWhere('feed_generator.description', 'ilike', `%${query}%`), ) } From 2e128869a41cd05cf82ed1dd93a9d4f382e5410f Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 20 Jul 2023 09:42:55 -0500 Subject: [PATCH 055/237] handle sqlite too in pds only --- packages/pds/src/app-view/api/app/bsky/unspecced.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/pds/src/app-view/api/app/bsky/unspecced.ts b/packages/pds/src/app-view/api/app/bsky/unspecced.ts index e9610d78291..7e3e025a2fd 100644 --- a/packages/pds/src/app-view/api/app/bsky/unspecced.ts +++ b/packages/pds/src/app-view/api/app/bsky/unspecced.ts @@ -148,11 +148,13 @@ export default function (server: Server, ctx: AppContext) { ]) if (query) { - inner = inner.where((qb) => + // like is case-insensitive is sqlite, and ilike is not supported + const operator = ctx.db.dialect === 'pg' ? 'ilike' : 'like' + inner = inner.where(qb => ( qb - .where('feed_generator.displayName', 'ilike', `%${query}%`) - .orWhere('feed_generator.description', 'ilike', `%${query}%`), - ) + .where('feed_generator.displayName', operator, `%${query}%`) + .orWhere('feed_generator.description', operator, `%${query}%`) + )) } let builder = ctx.db.db.selectFrom(inner.as('feed_gens')).selectAll() From 4a9a53866c7b0ea9112861b2fc201c99a8492bc9 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 20 Jul 2023 10:46:00 -0500 Subject: [PATCH 056/237] fix lint --- packages/pds/src/app-view/api/app/bsky/unspecced.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/pds/src/app-view/api/app/bsky/unspecced.ts b/packages/pds/src/app-view/api/app/bsky/unspecced.ts index 7e3e025a2fd..ab4f22649a9 100644 --- a/packages/pds/src/app-view/api/app/bsky/unspecced.ts +++ b/packages/pds/src/app-view/api/app/bsky/unspecced.ts @@ -150,11 +150,11 @@ export default function (server: Server, ctx: AppContext) { if (query) { // like is case-insensitive is sqlite, and ilike is not supported const operator = ctx.db.dialect === 'pg' ? 'ilike' : 'like' - inner = inner.where(qb => ( + inner = inner.where((qb) => qb .where('feed_generator.displayName', operator, `%${query}%`) - .orWhere('feed_generator.description', operator, `%${query}%`) - )) + .orWhere('feed_generator.description', operator, `%${query}%`), + ) } let builder = ctx.db.db.selectFrom(inner.as('feed_gens')).selectAll() From 354e05225b7519099e71b59f0939a835b90e97c4 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 20 Jul 2023 12:35:42 -0500 Subject: [PATCH 057/237] v0.4.3 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index bdc201933d2..3ee3e2a824c 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.4.2", + "version": "0.4.3", "main": "src/index.ts", "scripts": { "codegen": "lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From 8457800a3e8d399484a2739d66a7e3e70f2130f0 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Fri, 21 Jul 2023 12:47:03 -0400 Subject: [PATCH 058/237] Include takendown posts for admins (feature branch) (#1361) * :construction: WIP including takendown posts on author feed * :sparkles: Add takedown id on posts when including taken down posts * :broom: Cleanup the auth verifier and other bsky package code * :white_check_mark: Add test for admin getAuthorFeed * :broom: Cleanup lexicon and exclude takedownId * more explicit plumbing for post hydration w/o requester or with takedown info * pass along flag for soft-deleted actors * cleanup getAuthorFeed w/ auth * reorg getAuthorFeed logic around role/access-based auth --------- Co-authored-by: Foysal Ahamed --- .../pds/src/api/com/atproto/sync/getBlob.ts | 2 +- .../pds/src/api/com/atproto/sync/getBlocks.ts | 2 +- .../src/api/com/atproto/sync/getCheckout.ts | 2 +- .../src/api/com/atproto/sync/getCommitPath.ts | 2 +- .../pds/src/api/com/atproto/sync/getHead.ts | 2 +- .../pds/src/api/com/atproto/sync/getRecord.ts | 2 +- .../pds/src/api/com/atproto/sync/getRepo.ts | 2 +- .../pds/src/api/com/atproto/sync/listBlobs.ts | 2 +- .../api/app/bsky/feed/getAuthorFeed.ts | 115 +++++++++------ .../pds/src/app-view/services/feed/index.ts | 70 +++++---- .../pds/src/app-view/services/feed/types.ts | 5 + .../pds/src/app-view/services/feed/views.ts | 20 ++- .../pds/src/app-view/services/graph/index.ts | 6 +- packages/pds/src/auth.ts | 29 +++- packages/pds/src/context.ts | 8 +- packages/pds/tests/views/author-feed.test.ts | 139 +++++++++++------- 16 files changed, 257 insertions(+), 151 deletions(-) diff --git a/packages/pds/src/api/com/atproto/sync/getBlob.ts b/packages/pds/src/api/com/atproto/sync/getBlob.ts index 49b18d91a4c..f5c5c55f839 100644 --- a/packages/pds/src/api/com/atproto/sync/getBlob.ts +++ b/packages/pds/src/api/com/atproto/sync/getBlob.ts @@ -7,7 +7,7 @@ import { isUserOrAdmin } from '../../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getBlob({ - auth: ctx.optionalAccessOrAdminVerifier, + auth: ctx.optionalAccessOrRoleVerifier, handler: async ({ params, res, auth }) => { const { ref } = ctx.db.db.dynamic const found = await ctx.db.db diff --git a/packages/pds/src/api/com/atproto/sync/getBlocks.ts b/packages/pds/src/api/com/atproto/sync/getBlocks.ts index 56ac1b7f78a..6e8a6805e69 100644 --- a/packages/pds/src/api/com/atproto/sync/getBlocks.ts +++ b/packages/pds/src/api/com/atproto/sync/getBlocks.ts @@ -9,7 +9,7 @@ import { isUserOrAdmin } from '../../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getBlocks({ - auth: ctx.optionalAccessOrAdminVerifier, + auth: ctx.optionalAccessOrRoleVerifier, handler: async ({ params, auth }) => { const { did } = params // takedown check for anyone other than an admin or the user diff --git a/packages/pds/src/api/com/atproto/sync/getCheckout.ts b/packages/pds/src/api/com/atproto/sync/getCheckout.ts index fd775e29c01..1512d24b12d 100644 --- a/packages/pds/src/api/com/atproto/sync/getCheckout.ts +++ b/packages/pds/src/api/com/atproto/sync/getCheckout.ts @@ -9,7 +9,7 @@ import { isUserOrAdmin } from '../../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getCheckout({ - auth: ctx.optionalAccessOrAdminVerifier, + auth: ctx.optionalAccessOrRoleVerifier, handler: async ({ params, auth }) => { const { did } = params // takedown check for anyone other than an admin or the user diff --git a/packages/pds/src/api/com/atproto/sync/getCommitPath.ts b/packages/pds/src/api/com/atproto/sync/getCommitPath.ts index b8cbbd4ec02..6adbc2b073b 100644 --- a/packages/pds/src/api/com/atproto/sync/getCommitPath.ts +++ b/packages/pds/src/api/com/atproto/sync/getCommitPath.ts @@ -7,7 +7,7 @@ import { isUserOrAdmin } from '../../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getCommitPath({ - auth: ctx.optionalAccessOrAdminVerifier, + auth: ctx.optionalAccessOrRoleVerifier, handler: async ({ params, auth }) => { const { did } = params // takedown check for anyone other than an admin or the user diff --git a/packages/pds/src/api/com/atproto/sync/getHead.ts b/packages/pds/src/api/com/atproto/sync/getHead.ts index 17740fa1e13..bc55ba41d74 100644 --- a/packages/pds/src/api/com/atproto/sync/getHead.ts +++ b/packages/pds/src/api/com/atproto/sync/getHead.ts @@ -6,7 +6,7 @@ import { isUserOrAdmin } from '../../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getHead({ - auth: ctx.optionalAccessOrAdminVerifier, + auth: ctx.optionalAccessOrRoleVerifier, handler: async ({ params, auth }) => { const { did } = params // takedown check for anyone other than an admin or the user diff --git a/packages/pds/src/api/com/atproto/sync/getRecord.ts b/packages/pds/src/api/com/atproto/sync/getRecord.ts index 873cebbe86c..9a9690012a5 100644 --- a/packages/pds/src/api/com/atproto/sync/getRecord.ts +++ b/packages/pds/src/api/com/atproto/sync/getRecord.ts @@ -9,7 +9,7 @@ import { isUserOrAdmin } from '../../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getRecord({ - auth: ctx.optionalAccessOrAdminVerifier, + auth: ctx.optionalAccessOrRoleVerifier, handler: async ({ params, auth }) => { const { did, collection, rkey } = params // takedown check for anyone other than an admin or the user diff --git a/packages/pds/src/api/com/atproto/sync/getRepo.ts b/packages/pds/src/api/com/atproto/sync/getRepo.ts index 431be8ee89a..fc89ffc8402 100644 --- a/packages/pds/src/api/com/atproto/sync/getRepo.ts +++ b/packages/pds/src/api/com/atproto/sync/getRepo.ts @@ -9,7 +9,7 @@ import { isUserOrAdmin } from '../../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getRepo({ - auth: ctx.optionalAccessOrAdminVerifier, + auth: ctx.optionalAccessOrRoleVerifier, handler: async ({ params, auth }) => { const { did } = params // takedown check for anyone other than an admin or the user diff --git a/packages/pds/src/api/com/atproto/sync/listBlobs.ts b/packages/pds/src/api/com/atproto/sync/listBlobs.ts index c67bce4c0c8..910695b2c9d 100644 --- a/packages/pds/src/api/com/atproto/sync/listBlobs.ts +++ b/packages/pds/src/api/com/atproto/sync/listBlobs.ts @@ -7,7 +7,7 @@ import { isUserOrAdmin } from '../../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.listBlobs({ - auth: ctx.optionalAccessOrAdminVerifier, + auth: ctx.optionalAccessOrRoleVerifier, handler: async ({ params, auth }) => { const { did } = params // takedown check for anyone other than an admin or the user diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index 6a349dd6ccf..67322431164 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -1,19 +1,27 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../../lexicon' import { FeedKeyset } from '../util/feed' import { paginate } from '../../../../../db/pagination' import AppContext from '../../../../../context' import { FeedRow } from '../../../../services/feed' -import { InvalidRequestError } from '@atproto/xrpc-server' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getAuthorFeed({ - auth: ctx.accessVerifier, + auth: ctx.accessOrRoleVerifier, handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did + const requester = + auth.credentials.type === 'access' ? auth.credentials.did : null if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed( params, - await ctx.serviceAuthHeaders(requester), + requester + ? await ctx.serviceAuthHeaders(requester) + : { + // @TODO use authPassthru() once it lands + headers: req.headers.authorization + ? { authorization: req.headers.authorization } + : {}, + }, ) return { encoding: 'application/json', @@ -22,52 +30,32 @@ export default function (server: Server, ctx: AppContext) { } const { actor, limit, cursor } = params - const db = ctx.db.db - const { ref } = db.dynamic - - // first verify there is not a block between requester & subject - const blocks = await ctx.services.appView - .graph(ctx.db) - .getBlocks(requester, actor) - if (blocks.blocking) { - throw new InvalidRequestError( - `Requester has blocked actor: ${actor}`, - 'BlockedActor', - ) - } else if (blocks.blockedBy) { - throw new InvalidRequestError( - `Requester is blocked by actor: $${actor}`, - 'BlockedByActor', - ) - } + const { ref } = ctx.db.db.dynamic const accountService = ctx.services.account(ctx.db) const feedService = ctx.services.appView.feed(ctx.db) const graphService = ctx.services.appView.graph(ctx.db) - const userLookupCol = actor.startsWith('did:') - ? 'did_handle.did' - : 'did_handle.handle' - const actorDidQb = db - .selectFrom('did_handle') - .select('did') - .where(userLookupCol, '=', actor) - .limit(1) + let feedItemsQb = getFeedItemsQb(ctx, { actor }) - let feedItemsQb = feedService - .selectFeedItemQb() - .where('originatorDid', '=', actorDidQb) - .where((qb) => - // Hide reposts of muted content - qb - .where('type', '=', 'post') - .orWhere((qb) => - accountService.whereNotMuted(qb, requester, [ - ref('post.creator'), - ]), - ), - ) - .whereNotExists(graphService.blockQb(requester, [ref('post.creator')])) + // for access-based auth, enforce blocks and mutes + if (requester) { + await assertNoBlocks(ctx, { requester, actor }) + feedItemsQb = feedItemsQb + .where((qb) => + // hide reposts of muted content + qb + .where('type', '=', 'post') + .orWhere((qb) => + accountService.whereNotMuted(qb, requester, [ + ref('post.creator'), + ]), + ), + ) + .whereNotExists( + graphService.blockQb(requester, [ref('post.creator')]), + ) + } const keyset = new FeedKeyset( ref('feed_item.sortAt'), @@ -81,7 +69,9 @@ export default function (server: Server, ctx: AppContext) { }) const feedItems: FeedRow[] = await feedItemsQb.execute() - const feed = await feedService.hydrateFeed(feedItems, requester) + const feed = await feedService.hydrateFeed(feedItems, requester, { + includeSoftDeleted: auth.credentials.type === 'role', // show takendown content to mods + }) return { encoding: 'application/json', @@ -93,3 +83,38 @@ export default function (server: Server, ctx: AppContext) { }, }) } + +function getFeedItemsQb(ctx: AppContext, opts: { actor: string }) { + const { actor } = opts + const feedService = ctx.services.appView.feed(ctx.db) + const userLookupCol = actor.startsWith('did:') + ? 'did_handle.did' + : 'did_handle.handle' + const actorDidQb = ctx.db.db + .selectFrom('did_handle') + .select('did') + .where(userLookupCol, '=', actor) + .limit(1) + return feedService.selectFeedItemQb().where('originatorDid', '=', actorDidQb) +} + +// throws when there's a block between the two users +async function assertNoBlocks( + ctx: AppContext, + opts: { requester: string; actor: string }, +) { + const { requester, actor } = opts + const graphService = ctx.services.appView.graph(ctx.db) + const blocks = await graphService.getBlocks(requester, actor) + if (blocks.blocking) { + throw new InvalidRequestError( + `Requester has blocked actor: ${actor}`, + 'BlockedActor', + ) + } else if (blocks.blockedBy) { + throw new InvalidRequestError( + `Requester is blocked by actor: $${actor}`, + 'BlockedByActor', + ) + } +} diff --git a/packages/pds/src/app-view/services/feed/index.ts b/packages/pds/src/app-view/services/feed/index.ts index 732ba6623b3..af4d41590a6 100644 --- a/packages/pds/src/app-view/services/feed/index.ts +++ b/packages/pds/src/app-view/services/feed/index.ts @@ -24,6 +24,7 @@ import { PostViews, PostEmbedViews, RecordEmbedViewRecordMap, + FeedHydrationOptions, } from './types' import { LabelService, Labels } from '../label' import { ActorService } from '../actor' @@ -79,7 +80,7 @@ export class FeedService { ]) } - selectFeedGeneratorQb(requester: string) { + selectFeedGeneratorQb(requester: string | null) { const { ref } = this.db.db.dynamic return this.db.db .selectFrom('feed_generator') @@ -103,7 +104,7 @@ export class FeedService { .select((qb) => qb .selectFrom('like') - .where('like.creator', '=', requester) + .where('like.creator', '=', requester ?? '') .whereRef('like.subject', '=', 'feed_generator.uri') .select('uri') .as('viewerLike'), @@ -113,7 +114,7 @@ export class FeedService { // @NOTE keep in sync with actorService.views.profile() async getActorInfos( dids: string[], - requester: string, + requester: string | null, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, // @NOTE used by hydrateFeed() to batch label hydration ): Promise { if (dids.length < 1) return {} @@ -137,38 +138,38 @@ export class FeedService { 'profile.indexedAt as indexedAt', this.db.db .selectFrom('follow') - .where('creator', '=', requester) + .where('creator', '=', requester ?? '') .whereRef('subjectDid', '=', ref('did_handle.did')) .select('uri') .as('requesterFollowing'), this.db.db .selectFrom('follow') .whereRef('creator', '=', ref('did_handle.did')) - .where('subjectDid', '=', requester) + .where('subjectDid', '=', requester ?? '') .select('uri') .as('requesterFollowedBy'), this.db.db .selectFrom('actor_block') - .where('creator', '=', requester) + .where('creator', '=', requester ?? '') .whereRef('subjectDid', '=', ref('did_handle.did')) .select('uri') .as('requesterBlocking'), this.db.db .selectFrom('actor_block') .whereRef('creator', '=', ref('did_handle.did')) - .where('subjectDid', '=', requester) + .where('subjectDid', '=', requester ?? '') .select('uri') .as('requesterBlockedBy'), this.db.db .selectFrom('mute') .whereRef('did', '=', ref('did_handle.did')) - .where('mutedByDid', '=', requester) + .where('mutedByDid', '=', requester ?? '') .select('did') .as('requesterMuted'), this.db.db .selectFrom('list_item') .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') - .where('list_mute.mutedByDid', '=', requester) + .where('list_mute.mutedByDid', '=', requester ?? '') .whereRef('list_item.subjectDid', '=', ref('did_handle.did')) .select('list_item.listUri') .limit(1) @@ -215,12 +216,13 @@ export class FeedService { async getPostInfos( postUris: string[], - requester: string, + requester: string | null, + options?: Pick, ): Promise { if (postUris.length < 1) return {} const db = this.db.db const { ref } = db.dynamic - const posts = await db + let postsQb = db .selectFrom('post') .where('post.uri', 'in', postUris) .leftJoin('post_agg', 'post_agg.uri', 'post.uri') @@ -231,8 +233,14 @@ export class FeedService { ) .innerJoin('repo_root', 'repo_root.did', 'post.creator') .innerJoin('record', 'record.uri', 'post.uri') - .where(notSoftDeletedClause(ref('repo_root'))) // Ensures post reply parent/roots get omitted from views when taken down - .where(notSoftDeletedClause(ref('record'))) + + if (!options?.includeSoftDeleted) { + postsQb = postsQb + .where(notSoftDeletedClause(ref('repo_root'))) // Ensures post reply parent/roots get omitted from views when taken down + .where(notSoftDeletedClause(ref('record'))) + } + + const posts = await postsQb .select([ 'post.uri as uri', 'post.cid as cid', @@ -242,15 +250,16 @@ export class FeedService { 'post_agg.likeCount as likeCount', 'post_agg.repostCount as repostCount', 'post_agg.replyCount as replyCount', + 'record.takedownId as takedownId', db .selectFrom('repost') - .where('creator', '=', requester) + .where('creator', '=', requester ?? '') .whereRef('subject', '=', ref('post.uri')) .select('uri') .as('requesterRepost'), db .selectFrom('like') - .where('creator', '=', requester) + .where('creator', '=', requester ?? '') .whereRef('subject', '=', ref('post.uri')) .select('uri') .as('requesterLike'), @@ -265,7 +274,10 @@ export class FeedService { ) } - async getFeedGeneratorInfos(generatorUris: string[], requester: string) { + async getFeedGeneratorInfos( + generatorUris: string[], + requester: string | null, + ) { if (generatorUris.length < 1) return {} const feedGens = await this.selectFeedGeneratorQb(requester) .where('feed_generator.uri', 'in', generatorUris) @@ -313,9 +325,8 @@ export class FeedService { async hydrateFeed( items: FeedRow[], - requester: string, - // @TODO (deprecated) remove this once all clients support the blocked/not-found union on post views - usePostViewUnion?: boolean, + requester: string | null, + options?: FeedHydrationOptions, ): Promise { const actorDids = new Set() const postUris = new Set() @@ -334,26 +345,25 @@ export class FeedService { actorDids.add(new AtUri(item.replyRoot).hostname) } } + const [actors, posts, labels] = await Promise.all([ this.getActorInfos(Array.from(actorDids), requester, { skipLabels: true, + includeSoftDeleted: options?.includeSoftDeleted, }), - this.getPostInfos(Array.from(postUris), requester), + this.getPostInfos(Array.from(postUris), requester, options), this.services.label.getLabelsForSubjects([...postUris, ...actorDids]), ]) const embeds = await this.embedsForPosts(posts, requester) - return this.views.formatFeed( - items, - actors, - posts, - embeds, - labels, - usePostViewUnion, - ) + return this.views.formatFeed(items, actors, posts, embeds, labels, options) } - async embedsForPosts(postInfos: PostInfoMap, requester: string, depth = 0) { + async embedsForPosts( + postInfos: PostInfoMap, + requester: string | null, + depth = 0, + ) { const postMap = postRecordsFromInfos(postInfos) const posts = Object.values(postMap) if (posts.length < 1) { @@ -392,7 +402,7 @@ export class FeedService { async nestedRecordViews( posts: PostRecord[], - requester: string, + requester: string | null, depth: number, ): Promise { const nestedUris = nestedRecordUris(posts) diff --git a/packages/pds/src/app-view/services/feed/types.ts b/packages/pds/src/app-view/services/feed/types.ts index d81f856a86e..32076ddb06c 100644 --- a/packages/pds/src/app-view/services/feed/types.ts +++ b/packages/pds/src/app-view/services/feed/types.ts @@ -40,6 +40,7 @@ export type PostInfo = { replyCount: number | null requesterRepost: string | null requesterLike: string | null + takedownId: number | null } export type PostInfoMap = { [uri: string]: PostInfo } @@ -80,6 +81,10 @@ export type FeedRow = { replyRoot: string | null sortAt: string } +export type FeedHydrationOptions = { + includeSoftDeleted?: boolean + usePostViewUnion?: boolean +} export type MaybePostView = PostView | NotFoundPost | BlockedPost diff --git a/packages/pds/src/app-view/services/feed/views.ts b/packages/pds/src/app-view/services/feed/views.ts index 21ed899a8c6..b145269f010 100644 --- a/packages/pds/src/app-view/services/feed/views.ts +++ b/packages/pds/src/app-view/services/feed/views.ts @@ -29,6 +29,7 @@ import { MaybePostView, PostInfoMap, RecordEmbedViewRecord, + FeedHydrationOptions, } from './types' import { Labels } from '../label' import { ProfileView } from '../../../lexicon/types/app/bsky/actor/defs' @@ -81,7 +82,7 @@ export class FeedViews { posts: PostInfoMap, embeds: PostEmbedViews, labels: Labels, - usePostViewUnion?: boolean, + opts?: FeedHydrationOptions, ): FeedViewPost[] { const feed: FeedViewPost[] = [] for (const item of items) { @@ -91,6 +92,7 @@ export class FeedViews { posts, embeds, labels, + opts, ) // skip over not found & blocked posts if (!post) { @@ -120,7 +122,7 @@ export class FeedViews { posts, embeds, labels, - usePostViewUnion, + opts, ) const replyRoot = this.formatMaybePostView( item.replyRoot, @@ -128,7 +130,7 @@ export class FeedViews { posts, embeds, labels, - usePostViewUnion, + opts, ) if (replyRoot && replyParent) { feedPost['reply'] = { @@ -148,6 +150,7 @@ export class FeedViews { posts: PostInfoMap, embeds: PostEmbedViews, labels: Labels, + opts?: Pick, ): PostView | undefined { const post = posts[uri] const author = actors[post?.creator] @@ -159,6 +162,9 @@ export class FeedViews { uri: post.uri, cid: post.cid, author: author, + takedownId: opts?.includeSoftDeleted + ? post.takedownId ?? null + : undefined, record: cborToLexRecord(post.recordBytes), embed: embeds[uri], replyCount: post.replyCount ?? 0, @@ -179,15 +185,15 @@ export class FeedViews { posts: PostInfoMap, embeds: PostEmbedViews, labels: Labels, - usePostViewUnion?: boolean, + opts?: FeedHydrationOptions, ): MaybePostView | undefined { - const post = this.formatPostView(uri, actors, posts, embeds, labels) + const post = this.formatPostView(uri, actors, posts, embeds, labels, opts) if (!post) { - if (!usePostViewUnion) return + if (!opts?.usePostViewUnion) return return this.notFoundPost(uri) } if (post.author.viewer?.blockedBy || post.author.viewer?.blocking) { - if (!usePostViewUnion) return + if (!opts?.usePostViewUnion) return return this.blockedPost(uri) } return { diff --git a/packages/pds/src/app-view/services/graph/index.ts b/packages/pds/src/app-view/services/graph/index.ts index 94f7c58f54d..ba8ac303f4c 100644 --- a/packages/pds/src/app-view/services/graph/index.ts +++ b/packages/pds/src/app-view/services/graph/index.ts @@ -13,7 +13,7 @@ export class GraphService { return (db: Database) => new GraphService(db, imgUriBuilder) } - getListsQb(requester: string) { + getListsQb(requester: string | null) { const { ref } = this.db.db.dynamic return this.db.db .selectFrom('list') @@ -23,7 +23,7 @@ export class GraphService { .select( this.db.db .selectFrom('list_mute') - .where('list_mute.mutedByDid', '=', requester) + .where('list_mute.mutedByDid', '=', requester ?? '') .whereRef('list_mute.listUri', '=', ref('list.uri')) .select('list_mute.listUri') .as('viewerMuted'), @@ -98,7 +98,7 @@ export class GraphService { } } - async getListViews(listUris: string[], requester: string) { + async getListViews(listUris: string[], requester: string | null) { if (listUris.length < 1) return {} const lists = await this.getListsQb(requester) .where('list.uri', 'in', listUris) diff --git a/packages/pds/src/auth.ts b/packages/pds/src/auth.ts index e5cb9185143..66c9b1f0780 100644 --- a/packages/pds/src/auth.ts +++ b/packages/pds/src/auth.ts @@ -227,7 +227,34 @@ export const accessVerifierCheckTakedown = } } -export const optionalAccessOrAdminVerifier = (auth: ServerAuth) => { +export const accessOrRoleVerifier = (auth: ServerAuth) => { + const verifyAccess = accessVerifier(auth) + const verifyRole = roleVerifier(auth) + return async (ctx: { req: express.Request; res: express.Response }) => { + // For non-admin tokens, we don't want to consider alternative verifiers and let it fail if it fails + const isRoleAuthToken = ctx.req.headers.authorization?.startsWith(BASIC) + if (isRoleAuthToken) { + const result = await verifyRole(ctx) + return { + ...result, + credentials: { + type: 'role' as const, + ...result.credentials, + }, + } + } + const result = await verifyAccess(ctx) + return { + ...result, + credentials: { + type: 'access' as const, + ...result.credentials, + }, + } + } +} + +export const optionalAccessOrRoleVerifier = (auth: ServerAuth) => { const verifyAccess = accessVerifier(auth) return async (ctx: { req: express.Request; res: express.Response }) => { try { diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index d315e10f1cc..910699bfd2b 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -97,8 +97,12 @@ export class AppContext { return auth.roleVerifier(this.auth) } - get optionalAccessOrAdminVerifier() { - return auth.optionalAccessOrAdminVerifier(this.auth) + get accessOrRoleVerifier() { + return auth.accessOrRoleVerifier(this.auth) + } + + get optionalAccessOrRoleVerifier() { + return auth.optionalAccessOrRoleVerifier(this.auth) } get imgUriBuilder(): ImageUriBuilder { diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index 64246f0fcf2..4af028dc03f 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -1,4 +1,4 @@ -import AtpAgent from '@atproto/api' +import AtpAgent, { ComAtprotoAdminTakeModerationAction } from '@atproto/api' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { runTestServer, @@ -21,6 +21,35 @@ describe('pds author feed views', () => { let carol: string let dan: string + const reverseModerationAction = async (id) => + agent.api.com.atproto.admin.reverseModerationAction( + { + id, + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: { authorization: adminAuth() }, + }, + ) + + const takedownSubject = async ( + subject: ComAtprotoAdminTakeModerationAction.InputSchema['subject'], + ) => + agent.api.com.atproto.admin.takeModerationAction( + { + action: TAKEDOWN, + subject, + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: { authorization: adminAuth() }, + }, + ) + beforeAll(async () => { const server = await runTestServer({ dbPostgresSchema: 'views_author_feed', @@ -159,22 +188,10 @@ describe('pds author feed views', () => { expect(preBlock.feed.length).toBeGreaterThan(0) - const { data: action } = - await agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: alice, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) + const { data: action } = await takedownSubject({ + $type: 'com.atproto.admin.defs#repoRef', + did: alice, + }) const { data: postBlock } = await agent.api.app.bsky.feed.getAuthorFeed( { actor: alice }, @@ -184,17 +201,7 @@ describe('pds author feed views', () => { expect(postBlock.feed.length).toEqual(0) // Cleanup - await agent.api.com.atproto.admin.reverseModerationAction( - { - id: action.id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) + await reverseModerationAction(action.id) }) it('blocked by record takedown.', async () => { @@ -207,23 +214,11 @@ describe('pds author feed views', () => { const post = preBlock.feed[0].post - const { data: action } = - await agent.api.com.atproto.admin.takeModerationAction( - { - action: TAKEDOWN, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: post.uri, - cid: post.cid, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, - ) + const { data: action } = await takedownSubject({ + $type: 'com.atproto.repo.strongRef', + uri: post.uri, + cid: post.cid, + }) const { data: postBlock } = await agent.api.app.bsky.feed.getAuthorFeed( { actor: alice }, @@ -234,16 +229,50 @@ describe('pds author feed views', () => { expect(postBlock.feed.map((item) => item.post.uri)).not.toContain(post.uri) // Cleanup - await agent.api.com.atproto.admin.reverseModerationAction( - { - id: action.id, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: { authorization: adminAuth() }, - }, + await reverseModerationAction(action.id) + }) + + it('includes takendown posts for admins', async () => { + const { data: preTakedown } = await agent.api.app.bsky.feed.getAuthorFeed( + { actor: alice }, + { headers: sc.getHeaders(carol) }, ) + + expect(preTakedown.feed.length).toBeGreaterThan(0) + + const post = preTakedown.feed[0].post + + const { data: takedownAction } = await takedownSubject({ + $type: 'com.atproto.repo.strongRef', + uri: post.uri, + cid: post.cid, + }) + + const [{ data: postTakedownForCarol }, { data: postTakedownForAdmin }] = + await Promise.all([ + agent.api.app.bsky.feed.getAuthorFeed( + { actor: alice }, + { headers: sc.getHeaders(carol) }, + ), + agent.api.app.bsky.feed.getAuthorFeed( + { actor: alice }, + { headers: { authorization: adminAuth() } }, + ), + ]) + + const takendownPostInCarolsFeed = postTakedownForCarol.feed.find( + (item) => item.post.uri === post.uri || !!post.takedownId, + ) + const takendownPostInAdminsFeed = postTakedownForAdmin.feed.find( + (item) => item.post.uri === post.uri || !!post.takedownId, + ) + expect(takendownPostInCarolsFeed).toBeFalsy() + expect(takendownPostInAdminsFeed).toBeTruthy() + expect(takendownPostInAdminsFeed?.post.takedownId).toEqual( + takedownAction.id, + ) + + // Cleanup + await reverseModerationAction(takedownAction.id) }) }) From 4619e2b24a39e55897c027c9138c98fd47fe7325 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Fri, 21 Jul 2023 18:50:41 +0200 Subject: [PATCH 059/237] :sparkles: Disable signing up with invite code from takendown account (#1350) --- .../api/com/atproto/server/createAccount.ts | 8 ++++ packages/pds/tests/invite-codes.test.ts | 44 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 6965eb596a0..af660d862f8 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -124,9 +124,17 @@ export const ensureCodeIsAvailable = async ( inviteCode: string, withLock = false, ): Promise => { + const { ref } = db.db.dynamic const invite = await db.db .selectFrom('invite_code') .selectAll() + .whereNotExists((qb) => + qb + .selectFrom('repo_root') + .selectAll() + .where('takedownId', 'is not', null) + .whereRef('did', '=', ref('invite_code.forUser')), + ) .where('code', '=', inviteCode) .if(withLock && db.dialect === 'pg', (qb) => qb.forUpdate().skipLocked()) .executeTakeFirst() diff --git a/packages/pds/tests/invite-codes.test.ts b/packages/pds/tests/invite-codes.test.ts index c52dbe76175..e9c34fcc800 100644 --- a/packages/pds/tests/invite-codes.test.ts +++ b/packages/pds/tests/invite-codes.test.ts @@ -4,6 +4,7 @@ import { AppContext } from '../src' import * as util from './_util' import { DAY } from '@atproto/common' import { genInvCodes } from '../src/api/com/atproto/server/util' +import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' describe('account', () => { let serverUrl: string @@ -45,6 +46,49 @@ describe('account', () => { ) }) + it('fails on invite code from takendown account', async () => { + const account = await makeLoggedInAccount(agent) + // assign an invite code to the user + const code = await createInviteCode(agent, 1, account.did) + // takedown the user's account + const { data: takedownAction } = + await agent.api.com.atproto.admin.takeModerationAction( + { + action: TAKEDOWN, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: account.did, + }, + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: { authorization: util.adminAuth() }, + }, + ) + // attempt to create account with the previously generated invite code + const promise = createAccountWithInvite(agent, code) + await expect(promise).rejects.toThrow( + ComAtprotoServerCreateAccount.InvalidInviteCodeError, + ) + + // double check that reversing the takedown action makes the invite code valid again + await agent.api.com.atproto.admin.reverseModerationAction( + { + id: takedownAction.id, + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: { authorization: util.adminAuth() }, + }, + ) + // attempt to create account with the previously generated invite code + await createAccountWithInvite(agent, code) + }) + it('fails on used up invite code', async () => { const code = await createInviteCode(agent, 2) await createAccountsWithInvite(agent, code, 2) From 23d701e72312ddaee034c7cffd38d48d5d8599aa Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 24 Jul 2023 13:56:31 -0500 Subject: [PATCH 060/237] Refactor appview media embeds (#1343) * refactor appview services * tidy types * Allow moderators to take and reverse actor takedowns (#1330) allow moderators to take and reverse actor takedowns * :sparkles: Allow searching reports by moderator did (#1283) * :sparkles: Allow searching reports by moderator did * :white_check_mark: Remove .only flags on tests * :white_check_mark: Update snapshot * :white_check_mark: Add checks for did match in actions * v0.4.1 * Make sequencer leader behavior optional on pds (#1250) * make sequencer leader behavior optional on pds * tidy * use 127.0.0.1 in with-test-db.sh for colima (#1297) So, since Docker Desktop has licensing issues, some folks use colima for running containers on their macOS machines (The licensing exempted CLI-only version of Docker only exists on Linux). Unfortunately, colima binds host ports only on the IPv4 localhost address (`127.0.0.1`) while the atproto postgres clients will attempt to connect to the IPv6 localhost address (`::1`) that macOS sets in /etc/hosts. See https://github.com/abiosoft/colima/issues/583 and https://github.com/lima-vm/lima/issues/1330 for the tickets against colima. (Docker Desktop binds to both IPv4 and IPv6 localhost addresses and so doesn't have this issue.) To workaround this silly issue, we can use `localhost` within the docker containers and docker-compose, but need to set the `DB_POSTGRES_URL` env var to use the IPv4 localhost explicitly. (Asking folks to edit /etc/hosts causes other tools to break and will be overridden on each OS upgrade.) * Subscription util tests (#1295) * consecutive list tests * flesh out subscription util tests --------- Co-authored-by: Devin Ivy * Content reporting on record fields (#1351) * content reporting on record fields * fix test * tests * tidy * Check rkey contents just for non-tids (#1353) check rkey content for non-tids * :sparkles: Added new procedure for sending admin email (#1312) * :construction: Added new lexicon for sending admin email * :sparkles: Add moderation mailer * :sparkles: Switch to text email content from html * :broom: Cleanup some early implementation code and reflect PR reivew * :sparkles: Use smtp host instead of gmail service config * :sparkles: Move to using single smtp url * v0.4.2 * Patch up a couple sqlite tests (#1355) patch up a couple sqlite tests * Increase appview keepalive (#1360) increase keepalive to 90s * Appview - serve feed skeletons (#1265) * proxy timeline skeleton construction to appview * add getFeedSkeleton to appview * mount route * smart proxy feed skeletons * tests * proper error code * only proxy specific feeds * build branch * update proxyable feed logic, should use feed publisher rather than generator * fix feed proxy tests, configure feed publisher (in addition to generator) * hotfix: prevent user-supplied rkey on posts with createRecord (#1313) * prevent user-supplied rkey on posts with createRecord * allow empty-string rkey parameter Co-authored-by: devin ivy --------- Co-authored-by: devin ivy * add slurs to reserved words (#1318) * add slurs to reserved words (#1314) * Update reserved.ts Add slurs to reserved words * Update reserved.ts fix typo * Update reserved.ts to clean up the slur list * linting * pluralise --------- Co-authored-by: jess * identifier: tweaks and additions to slur list (#1319) * Refactor appview repo subscription for memleak (#1308) * refactor to remove closure in loop * move consecutive item out of p-queue * Handle validation improvements (#1336) * Handle matches and false positives for unacceptable words in handles * move handle validation logic to pds * missed merge * add cfg flag * encode lists * fix build issues * move words to cfg * tidy --------- Co-authored-by: Jaz Volpert * Allow moderators to take and reverse actor takedowns (#1330) allow moderators to take and reverse actor takedowns * :sparkles: Allow searching reports by moderator did (#1283) * :sparkles: Allow searching reports by moderator did * :white_check_mark: Remove .only flags on tests * :white_check_mark: Update snapshot * :white_check_mark: Add checks for did match in actions * v0.4.1 * Make sequencer leader behavior optional on pds (#1250) * make sequencer leader behavior optional on pds * tidy * use 127.0.0.1 in with-test-db.sh for colima (#1297) So, since Docker Desktop has licensing issues, some folks use colima for running containers on their macOS machines (The licensing exempted CLI-only version of Docker only exists on Linux). Unfortunately, colima binds host ports only on the IPv4 localhost address (`127.0.0.1`) while the atproto postgres clients will attempt to connect to the IPv6 localhost address (`::1`) that macOS sets in /etc/hosts. See https://github.com/abiosoft/colima/issues/583 and https://github.com/lima-vm/lima/issues/1330 for the tickets against colima. (Docker Desktop binds to both IPv4 and IPv6 localhost addresses and so doesn't have this issue.) To workaround this silly issue, we can use `localhost` within the docker containers and docker-compose, but need to set the `DB_POSTGRES_URL` env var to use the IPv4 localhost explicitly. (Asking folks to edit /etc/hosts causes other tools to break and will be overridden on each OS upgrade.) * Subscription util tests (#1295) * consecutive list tests * flesh out subscription util tests --------- Co-authored-by: Devin Ivy * Content reporting on record fields (#1351) * content reporting on record fields * fix test * tests * tidy * Check rkey contents just for non-tids (#1353) check rkey content for non-tids * :sparkles: Added new procedure for sending admin email (#1312) * :construction: Added new lexicon for sending admin email * :sparkles: Add moderation mailer * :sparkles: Switch to text email content from html * :broom: Cleanup some early implementation code and reflect PR reivew * :sparkles: Use smtp host instead of gmail service config * :sparkles: Move to using single smtp url * v0.4.2 * Patch up a couple sqlite tests (#1355) patch up a couple sqlite tests * enable feeds & build branch * disable branch building & enable without proxy header --------- Co-authored-by: Devin Ivy Co-authored-by: David Buchanan Co-authored-by: jess Co-authored-by: bnewbold Co-authored-by: Jaz Volpert Co-authored-by: Foysal Ahamed Co-authored-by: Jeff Hodges * add optional ilike query to getPopularFeedGenerators * fix lint * fix lint * handle sqlite too in pds only * fix lint * v0.4.3 * Include takendown posts for admins (feature branch) (#1361) * :construction: WIP including takendown posts on author feed * :sparkles: Add takedown id on posts when including taken down posts * :broom: Cleanup the auth verifier and other bsky package code * :white_check_mark: Add test for admin getAuthorFeed * :broom: Cleanup lexicon and exclude takedownId * more explicit plumbing for post hydration w/o requester or with takedown info * pass along flag for soft-deleted actors * cleanup getAuthorFeed w/ auth * reorg getAuthorFeed logic around role/access-based auth --------- Co-authored-by: Foysal Ahamed * :sparkles: Disable signing up with invite code from takendown account (#1350) * fix viewer data --------- Co-authored-by: Devin Ivy Co-authored-by: Foysal Ahamed Co-authored-by: Jeff Hodges Co-authored-by: David Buchanan Co-authored-by: jess Co-authored-by: bnewbold Co-authored-by: Jaz Volpert Co-authored-by: Eric Bailey --- .../src/api/app/bsky/feed/getActorFeeds.ts | 10 +- .../bsky/src/api/app/bsky/feed/getFeed.ts | 2 +- .../src/api/app/bsky/feed/getFeedGenerator.ts | 4 +- .../api/app/bsky/feed/getFeedGenerators.ts | 6 +- .../src/api/app/bsky/feed/getPostThread.ts | 21 +- .../bsky/src/api/app/bsky/feed/getPosts.ts | 26 +- .../unspecced/getPopularFeedGenerators.ts | 4 +- packages/bsky/src/api/app/bsky/util/feed.ts | 2 +- packages/bsky/src/feed-gen/types.ts | 2 +- packages/bsky/src/feed-gen/whats-hot.ts | 2 +- packages/bsky/src/services/feed/index.ts | 441 +++++++++--------- packages/bsky/src/services/feed/types.ts | 85 +++- packages/bsky/src/services/feed/views.ts | 246 +++++++--- packages/bsky/src/services/graph/index.ts | 20 + packages/bsky/src/services/types.ts | 49 -- 15 files changed, 537 insertions(+), 383 deletions(-) delete mode 100644 packages/bsky/src/services/types.ts diff --git a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts index 0a18d5b155a..5ca0a141585 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts @@ -39,9 +39,13 @@ export default function (server: Server, ctx: AppContext) { ]) const profiles = { [creatorProfile.did]: creatorProfile } - const feeds = feedsRes.map((row) => - feedService.views.formatFeedGeneratorView(row, profiles), - ) + const feeds = feedsRes.map((row) => { + const feed = { + ...row, + viewer: viewer ? { like: row.viewerLike } : undefined, + } + return feedService.views.formatFeedGeneratorView(feed, profiles) + }) return { encoding: 'application/json', diff --git a/packages/bsky/src/api/app/bsky/feed/getFeed.ts b/packages/bsky/src/api/app/bsky/feed/getFeed.ts index a2d49203e3b..6602e937a58 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeed.ts @@ -16,7 +16,7 @@ import { QueryParams as GetFeedParams } from '../../../../lexicon/types/app/bsky import { OutputSchema as SkeletonOutput } from '../../../../lexicon/types/app/bsky/feed/getFeedSkeleton' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { FeedRow } from '../../../../services/types' +import { FeedRow } from '../../../../services/feed/types' import { AlgoResponse } from '../../../../feed-gen/types' export default function (server: Server, ctx: AppContext) { diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts b/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts index 01f14b9349a..c3eeb14f8c4 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts @@ -16,7 +16,7 @@ export default function (server: Server, ctx: AppContext) { const feedService = ctx.services.feed(ctx.db) - const got = await feedService.getFeedGeneratorViews([feed], viewer) + const got = await feedService.getFeedGeneratorInfos([feed], viewer) const feedInfo = got[feed] if (!feedInfo) { throw new InvalidRequestError('could not find feed') @@ -45,7 +45,7 @@ export default function (server: Server, ctx: AppContext) { ) } - const profiles = await feedService.getActorViews( + const profiles = await feedService.getActorInfos( [feedInfo.creator], viewer, ) diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts b/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts index ee19a1050d2..f03f352a2eb 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts @@ -10,11 +10,11 @@ export default function (server: Server, ctx: AppContext) { const feedService = ctx.services.feed(ctx.db) - const genViews = await feedService.getFeedGeneratorViews(feeds, requester) - const genList = Object.values(genViews) + const genInfos = await feedService.getFeedGeneratorInfos(feeds, requester) + const genList = Object.values(genInfos) const creators = genList.map((gen) => gen.creator) - const profiles = await feedService.getActorViews(creators, requester) + const profiles = await feedService.getActorInfos(creators, requester) const feedViews = genList.map((gen) => feedService.views.formatFeedGeneratorView(gen, profiles), diff --git a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts index fc1723f27f7..351fc30fe56 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts @@ -2,12 +2,11 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { - ActorViewMap, - FeedEmbeds, FeedRow, - PostInfoMap, -} from '../../../../services/types' -import { FeedService } from '../../../../services/feed' + ActorInfoMap, + PostEmbedViews, +} from '../../../../services/feed/types' +import { FeedService, PostInfoMap } from '../../../../services/feed' import { Labels } from '../../../../services/label' import { BlockedPost, @@ -41,14 +40,14 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Post not found: ${uri}`, 'NotFound') } const relevant = getRelevantIds(threadData) - const [actors, posts, embeds, labels] = await Promise.all([ - feedService.getActorViews(Array.from(relevant.dids), requester, { + const [actors, posts, labels] = await Promise.all([ + feedService.getActorInfos(Array.from(relevant.dids), requester, { skipLabels: true, }), - feedService.getPostViews(Array.from(relevant.uris), requester), - feedService.embedsForPosts(Array.from(relevant.uris), requester), + feedService.getPostInfos(Array.from(relevant.uris), requester), labelService.getLabelsForSubjects([...relevant.uris, ...relevant.dids]), ]) + const embeds = await feedService.embedsForPosts(posts, requester) const thread = composeThread( threadData, @@ -76,8 +75,8 @@ const composeThread = ( threadData: PostThread, feedService: FeedService, posts: PostInfoMap, - actors: ActorViewMap, - embeds: FeedEmbeds, + actors: ActorInfoMap, + embeds: PostEmbedViews, labels: Labels, ) => { const post = feedService.views.formatPostView( diff --git a/packages/bsky/src/api/app/bsky/feed/getPosts.ts b/packages/bsky/src/api/app/bsky/feed/getPosts.ts index 16dcbb1f3d9..505eb25d739 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPosts.ts @@ -1,7 +1,6 @@ import * as common from '@atproto/common' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { AtUri } from '@atproto/uri' import { PostView } from '@atproto/api/src/client/types/app/bsky/feed/defs' export default function (server: Server, ctx: AppContext) { @@ -10,32 +9,15 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const requester = auth.credentials.did - const feedService = ctx.services.feed(ctx.db) - const labelService = ctx.services.label(ctx.db) - const uris = common.dedupeStrs(params.uris) - const dids = common.dedupeStrs( - params.uris.map((uri) => new AtUri(uri).hostname), - ) - const [actors, postViews, embeds, labels] = await Promise.all([ - feedService.getActorViews(Array.from(dids), requester, { - skipLabels: true, - }), - feedService.getPostViews(Array.from(uris), requester), - feedService.embedsForPosts(Array.from(uris), requester), - labelService.getLabelsForSubjects([...uris, ...dids]), - ]) + const postViews = await ctx.services + .feed(ctx.db) + .getPostViews(uris, requester) const posts: PostView[] = [] for (const uri of uris) { - const post = feedService.views.formatPostView( - uri, - actors, - postViews, - embeds, - labels, - ) + const post = postViews[uri] if (post) { posts.push(post) } diff --git a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index 2f665f06429..a29a21b687a 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -43,13 +43,13 @@ export default function (server: Server, ctx: AppContext) { const res = await builder.execute() - const genInfos = await feedService.getFeedGeneratorViews( + const genInfos = await feedService.getFeedGeneratorInfos( res.map((feed) => feed.uri), requester, ) const creators = Object.values(genInfos).map((gen) => gen.creator) - const profiles = await feedService.getActorViews(creators, requester) + const profiles = await feedService.getActorInfos(creators, requester) const genViews: GeneratorView[] = [] for (const row of res) { diff --git a/packages/bsky/src/api/app/bsky/util/feed.ts b/packages/bsky/src/api/app/bsky/util/feed.ts index 384f67e32e9..769b2d7e833 100644 --- a/packages/bsky/src/api/app/bsky/util/feed.ts +++ b/packages/bsky/src/api/app/bsky/util/feed.ts @@ -1,5 +1,5 @@ import { TimeCidKeyset } from '../../../../db/pagination' -import { FeedRow } from '../../../../services/types' +import { FeedRow } from '../../../../services/feed/types' export enum FeedAlgorithm { ReverseChronological = 'reverse-chronological', diff --git a/packages/bsky/src/feed-gen/types.ts b/packages/bsky/src/feed-gen/types.ts index e278c9a80d9..9670dff6018 100644 --- a/packages/bsky/src/feed-gen/types.ts +++ b/packages/bsky/src/feed-gen/types.ts @@ -1,6 +1,6 @@ import AppContext from '../context' import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' -import { FeedRow } from '../services/types' +import { FeedRow } from '../services/feed/types' export type AlgoResponse = { feedItems: FeedRow[] diff --git a/packages/bsky/src/feed-gen/whats-hot.ts b/packages/bsky/src/feed-gen/whats-hot.ts index 92771581865..126f4a66e4f 100644 --- a/packages/bsky/src/feed-gen/whats-hot.ts +++ b/packages/bsky/src/feed-gen/whats-hot.ts @@ -6,7 +6,7 @@ import { GenericKeyset, paginate } from '../db/pagination' import AppContext from '../context' import { valuesList } from '../db/util' import { sql } from 'kysely' -import { FeedItemType } from '../services/types' +import { FeedItemType } from '../services/feed/types' const NO_WHATS_HOT_LABELS: NotEmptyArray = [ '!no-promote', diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index d9beb7c7871..81d3937ba4e 100644 --- a/packages/bsky/src/services/feed/index.ts +++ b/packages/bsky/src/services/feed/index.ts @@ -2,31 +2,36 @@ import { sql } from 'kysely' import { AtUri } from '@atproto/uri' import { dedupeStrs } from '@atproto/common' import { INVALID_HANDLE } from '@atproto/identifier' +import { jsonStringToLex } from '@atproto/lexicon' import Database from '../../db' import { countAll, noMatch, notSoftDeletedClause } from '../../db/util' import { ImageUriBuilder } from '../../image/uri' import { ids } from '../../lexicon/lexicons' -import { isView as isViewImages } from '../../lexicon/types/app/bsky/embed/images' -import { isView as isViewExternal } from '../../lexicon/types/app/bsky/embed/external' import { - ViewRecord, - View as RecordEmbedView, - ViewNotFound, - ViewBlocked, -} from '../../lexicon/types/app/bsky/embed/record' -import { FeedViewPost, PostView } from '../../lexicon/types/app/bsky/feed/defs' + Record as PostRecord, + isRecord as isPostRecord, +} from '../../lexicon/types/app/bsky/feed/post' +import { isMain as isEmbedImages } from '../../lexicon/types/app/bsky/embed/images' +import { isMain as isEmbedExternal } from '../../lexicon/types/app/bsky/embed/external' +import { isMain as isEmbedRecord } from '../../lexicon/types/app/bsky/embed/record' +import { isMain as isEmbedRecordWithMedia } from '../../lexicon/types/app/bsky/embed/recordWithMedia' +import { FeedViewPost } from '../../lexicon/types/app/bsky/feed/defs' import { - ActorViewMap, - FeedEmbeds, + ActorInfoMap, PostInfoMap, FeedItemType, FeedRow, -} from '../types' -import { LabelService } from '../label' + FeedGenInfoMap, + PostViews, + PostEmbedViews, + RecordEmbedViewRecordMap, +} from './types' +import { LabelService, Labels } from '../label' import { ActorService } from '../actor' import { GraphService } from '../graph' import { FeedViews } from './views' -import { FeedGenInfoMap } from './types' + +export * from './types' export class FeedService { constructor(public db: Database, public imgUriBuilder: ImageUriBuilder) {} @@ -100,15 +105,15 @@ export class FeedService { // @TODO just use actor service?? // @NOTE keep in sync with actorService.views.profile() - async getActorViews( + async getActorInfos( dids: string[], viewer: string | null, opts?: { skipLabels?: boolean }, // @NOTE used by hydrateFeed() to batch label hydration - ): Promise { + ): Promise { if (dids.length < 1) return {} const { ref } = this.db.db.dynamic const { skipLabels } = opts ?? {} - const [actors, labels, listMutes] = await Promise.all([ + const [actors, labels] = await Promise.all([ this.db.db .selectFrom('actor') .leftJoin('profile', 'profile.creator', 'actor.did') @@ -156,11 +161,23 @@ export class FeedService { .where('mutedByDid', '=', viewer ?? '') .select('subjectDid') .as('requesterMuted'), + this.db.db + .selectFrom('list_item') + .if(!viewer, (q) => q.where(noMatch)) + .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') + .where('list_mute.mutedByDid', '=', viewer ?? '') + .whereRef('list_item.subjectDid', '=', ref('actor.did')) + .select('list_item.listUri') + .limit(1) + .as('requesterMutedByList'), ]) .execute(), this.services.label.getLabelsForSubjects(skipLabels ? [] : dids), - this.services.actor.views.getListMutes(dids, viewer), ]) + const listUris: string[] = actors + .map((a) => a.requesterMutedByList) + .filter((list) => !!list) + const listViews = await this.services.graph.getListViews(listUris, viewer) return actors.reduce((acc, cur) => { const actorLabels = labels[cur.did] ?? [] return { @@ -178,8 +195,12 @@ export class FeedService { : undefined, viewer: viewer ? { - muted: !!cur?.requesterMuted || !!listMutes[cur.did], - mutedByList: listMutes[cur.did], + muted: !!cur?.requesterMuted || !!cur?.requesterMutedByList, + mutedByList: cur.requesterMutedByList + ? this.services.graph.formatListViewBasic( + listViews[cur.requesterMutedByList], + ) + : undefined, blockedBy: !!cur?.requesterBlockedBy, blocking: cur?.requesterBlocking || undefined, following: cur?.requesterFollowing || undefined, @@ -189,10 +210,10 @@ export class FeedService { labels: skipLabels ? undefined : actorLabels, }, } - }, {} as ActorViewMap) + }, {} as ActorInfoMap) } - async getPostViews( + async getPostInfos( postUris: string[], viewer: string | null, ): Promise { @@ -241,7 +262,7 @@ export class FeedService { ) } - async getFeedGeneratorViews(generatorUris: string[], viewer: string | null) { + async getFeedGeneratorInfos(generatorUris: string[], viewer: string | null) { if (generatorUris.length < 1) return {} const feedGens = await this.selectFeedGeneratorQb(viewer) .where('feed_generator.uri', 'in', generatorUris) @@ -249,184 +270,45 @@ export class FeedService { return feedGens.reduce( (acc, cur) => ({ ...acc, - [cur.uri]: cur, + [cur.uri]: { + ...cur, + viewer: viewer ? { like: cur.viewerLike } : undefined, + }, }), {} as FeedGenInfoMap, ) } - async embedsForPosts( - uris: string[], - viewer: string | null, - _depth = 0, - ): Promise { - if (uris.length < 1 || _depth > 1) { - // If a post has a record embed which contains additional embeds, the depth check - // above ensures that we don't recurse indefinitely into those additional embeds. - // In short, you receive up to two layers of embeds for the post: this allows us to - // handle the case that a post has a record embed, which in turn has images embedded in it. - return {} - } - const imgPromise = this.db.db - .selectFrom('post_embed_image') - .selectAll() - .where('postUri', 'in', uris) - .orderBy('postUri') - .orderBy('position') - .execute() - const extPromise = this.db.db - .selectFrom('post_embed_external') - .selectAll() - .where('postUri', 'in', uris) - .execute() - const recordPromise = this.db.db - .selectFrom('post_embed_record') - .innerJoin('record as embed', 'embed.uri', 'embedUri') - .where('postUri', 'in', uris) - .select(['postUri', 'embed.uri as uri', 'embed.did as did']) - .execute() - const [images, externals, records] = await Promise.all([ - imgPromise, - extPromise, - recordPromise, - ]) - const nestedUris = dedupeStrs(records.map((p) => p.uri)) - const nestedDids = dedupeStrs(records.map((p) => p.did)) - const nestedPostUris = nestedUris.filter( - (uri) => new AtUri(uri).collection === ids.AppBskyFeedPost, - ) - const nestedFeedGenUris = nestedUris.filter( - (uri) => new AtUri(uri).collection === ids.AppBskyFeedGenerator, - ) - const nestedListUris = nestedUris.filter( - (uri) => new AtUri(uri).collection === ids.AppBskyGraphList, - ) - const [ - postViews, - actorViews, - deepEmbedViews, - labelViews, - feedGenViews, - listViews, - ] = await Promise.all([ - this.getPostViews(nestedPostUris, viewer), - this.getActorViews(nestedDids, viewer, { skipLabels: true }), - this.embedsForPosts(nestedUris, viewer, _depth + 1), - this.services.label.getLabelsForSubjects([...nestedUris, ...nestedDids]), - this.getFeedGeneratorViews(nestedFeedGenUris, viewer), - this.services.graph.getListViews(nestedListUris, viewer), + async getPostViews( + postUris: string[], + requester: string | null, + precomputed?: { + actors?: ActorInfoMap + posts?: PostInfoMap + embeds?: PostEmbedViews + labels?: Labels + }, + ): Promise { + const uris = dedupeStrs(postUris) + const dids = dedupeStrs(postUris.map((uri) => new AtUri(uri).hostname)) + + const [actors, posts, labels] = await Promise.all([ + precomputed?.actors ?? + this.getActorInfos(dids, requester, { skipLabels: true }), + precomputed?.posts ?? this.getPostInfos(uris, requester), + precomputed?.labels ?? + this.services.label.getLabelsForSubjects([...uris, ...dids]), ]) - let embeds = images.reduce((acc, cur) => { - const embed = (acc[cur.postUri] ??= { - $type: 'app.bsky.embed.images#view', - images: [], - }) - if (!isViewImages(embed)) return acc - const postUri = new AtUri(cur.postUri) - embed.images.push({ - thumb: this.imgUriBuilder.getCommonSignedUri( - 'feed_thumbnail', - postUri.host, - cur.imageCid, - ), - fullsize: this.imgUriBuilder.getCommonSignedUri( - 'feed_fullsize', - postUri.host, - cur.imageCid, - ), - alt: cur.alt, - }) - return acc - }, {} as FeedEmbeds) - embeds = externals.reduce((acc, cur) => { - if (!acc[cur.postUri]) { - const postUri = new AtUri(cur.postUri) - acc[cur.postUri] = { - $type: 'app.bsky.embed.external#view', - external: { - uri: cur.uri, - title: cur.title, - description: cur.description, - thumb: cur.thumbCid - ? this.imgUriBuilder.getCommonSignedUri( - 'feed_thumbnail', - postUri.host, - cur.thumbCid, - ) - : undefined, - }, - } - } - return acc - }, embeds) - embeds = records.reduce((acc, cur) => { - const collection = new AtUri(cur.uri).collection - let recordEmbed: RecordEmbedView - if (collection === ids.AppBskyFeedGenerator && feedGenViews[cur.uri]) { - recordEmbed = { - record: { - $type: 'app.bsky.feed.defs#generatorView', - ...this.views.formatFeedGeneratorView( - feedGenViews[cur.uri], - actorViews, - labelViews, - ), - }, - } - } else if (collection === ids.AppBskyGraphList && listViews[cur.uri]) { - recordEmbed = { - record: { - $type: 'app.bsky.graph.defs#listView', - ...this.services.graph.formatListView( - listViews[cur.uri], - actorViews, - ), - }, - } - } else if (collection === ids.AppBskyFeedPost && postViews[cur.uri]) { - const formatted = this.views.formatPostView( - cur.uri, - actorViews, - postViews, - deepEmbedViews, - labelViews, - ) - let deepEmbeds: ViewRecord['embeds'] | undefined - if (_depth < 1) { - // Omit field entirely when too deep: e.g. don't include it on the embeds within a record embed. - // Otherwise list any embeds that appear within the record. A consumer may discover an embed - // within the raw record, then look within this array to find the presented view of it. - deepEmbeds = formatted?.embed ? [formatted.embed] : [] - } - recordEmbed = { - record: getRecordEmbedView(cur.uri, formatted, deepEmbeds), - } - } else { - recordEmbed = { - record: { - $type: 'app.bsky.embed.record#viewNotFound', - uri: cur.uri, - }, - } - } - if (acc[cur.postUri]) { - const mediaEmbed = acc[cur.postUri] - if (isViewImages(mediaEmbed) || isViewExternal(mediaEmbed)) { - acc[cur.postUri] = { - $type: 'app.bsky.embed.recordWithMedia#view', - record: recordEmbed, - media: mediaEmbed, - } - } - } else { - acc[cur.postUri] = { - $type: 'app.bsky.embed.record#view', - ...recordEmbed, - } + const embeds = + precomputed?.embeds ?? (await this.embedsForPosts(posts, requester)) + + return uris.reduce((acc, cur) => { + const view = this.views.formatPostView(cur, actors, posts, embeds, labels) + if (view) { + acc[cur] = view } return acc - }, embeds) - return embeds + }, {} as PostViews) } async hydrateFeed( @@ -452,14 +334,14 @@ export class FeedService { actorDids.add(new AtUri(item.replyRoot).hostname) } } - const [actors, posts, embeds, labels] = await Promise.all([ - this.getActorViews(Array.from(actorDids), viewer, { + const [actors, posts, labels] = await Promise.all([ + this.getActorInfos(Array.from(actorDids), viewer, { skipLabels: true, }), - this.getPostViews(Array.from(postUris), viewer), - this.embedsForPosts(Array.from(postUris), viewer), + this.getPostInfos(Array.from(postUris), viewer), this.services.label.getLabelsForSubjects([...postUris, ...actorDids]), ]) + const embeds = await this.embedsForPosts(posts, viewer) return this.views.formatFeed( items, @@ -470,33 +352,154 @@ export class FeedService { usePostViewUnion, ) } -} -function getRecordEmbedView( - uri: string, - post?: PostView, - embeds?: ViewRecord['embeds'], -): (ViewRecord | ViewNotFound | ViewBlocked) & { $type: string } { - if (!post) { - return { - $type: 'app.bsky.embed.record#viewNotFound', - uri, + async embedsForPosts( + postInfos: PostInfoMap, + viewer: string | null, + depth = 0, + ) { + const postMap = postRecordsFromInfos(postInfos) + const posts = Object.values(postMap) + if (posts.length < 1) { + return {} + } + const recordEmbedViews = + depth > 1 ? {} : await this.nestedRecordViews(posts, viewer, depth) + + const postEmbedViews: PostEmbedViews = {} + for (const [uri, post] of Object.entries(postMap)) { + const creator = new AtUri(uri).hostname + if (!post.embed) continue + if (isEmbedImages(post.embed)) { + postEmbedViews[uri] = this.views.imagesEmbedView(creator, post.embed) + } else if (isEmbedExternal(post.embed)) { + postEmbedViews[uri] = this.views.externalEmbedView(creator, post.embed) + } else if (isEmbedRecord(post.embed)) { + if (!recordEmbedViews[post.embed.record.uri]) continue + postEmbedViews[uri] = { + $type: 'app.bsky.embed.record#view', + record: recordEmbedViews[post.embed.record.uri], + } + } else if (isEmbedRecordWithMedia(post.embed)) { + const embedRecordView = recordEmbedViews[post.embed.record.record.uri] + if (!embedRecordView) continue + const formatted = this.views.getRecordWithMediaEmbedView( + creator, + post.embed, + embedRecordView, + ) + if (formatted) { + postEmbedViews[uri] = formatted + } + } + } + return postEmbedViews + } + + async nestedRecordViews( + posts: PostRecord[], + viewer: string | null, + depth: number, + ): Promise { + const nestedUris = nestedRecordUris(posts) + if (nestedUris.length < 1) return {} + const nestedPostUris: string[] = [] + const nestedFeedGenUris: string[] = [] + const nestedListUris: string[] = [] + const nestedDidsSet = new Set() + for (const uri of nestedUris) { + const parsed = new AtUri(uri) + nestedDidsSet.add(parsed.hostname) + if (parsed.collection === ids.AppBskyFeedPost) { + nestedPostUris.push(uri) + } else if (parsed.collection === ids.AppBskyFeedGenerator) { + nestedFeedGenUris.push(uri) + } else if (parsed.collection === ids.AppBskyGraphList) { + nestedListUris.push(uri) + } } + const nestedDids = [...nestedDidsSet] + const [postInfos, actorInfos, labelViews, feedGenInfos, listViews] = + await Promise.all([ + this.getPostInfos(nestedPostUris, viewer), + this.getActorInfos(nestedDids, viewer, { skipLabels: true }), + this.services.label.getLabelsForSubjects([ + ...nestedPostUris, + ...nestedDids, + ]), + this.getFeedGeneratorInfos(nestedFeedGenUris, viewer), + this.services.graph.getListViews(nestedListUris, viewer), + ]) + const deepEmbedViews = await this.embedsForPosts( + postInfos, + viewer, + depth + 1, + ) + const recordEmbedViews: RecordEmbedViewRecordMap = {} + for (const uri of nestedUris) { + const collection = new AtUri(uri).collection + if (collection === ids.AppBskyFeedGenerator && feedGenInfos[uri]) { + recordEmbedViews[uri] = { + $type: 'app.bsky.feed.defs#generatorView', + ...this.views.formatFeedGeneratorView( + feedGenInfos[uri], + actorInfos, + labelViews, + ), + } + } else if (collection === ids.AppBskyGraphList && listViews[uri]) { + recordEmbedViews[uri] = { + $type: 'app.bsky.graph.defs#listView', + ...this.services.graph.formatListView(listViews[uri], actorInfos), + } + } else if (collection === ids.AppBskyFeedPost && postInfos[uri]) { + const formatted = this.views.formatPostView( + uri, + actorInfos, + postInfos, + deepEmbedViews, + labelViews, + ) + recordEmbedViews[uri] = this.views.getRecordEmbedView( + uri, + formatted, + depth > 0, + ) + } else { + recordEmbedViews[uri] = { + $type: 'app.bsky.embed.record#viewNotFound', + uri, + } + } + } + return recordEmbedViews } - if (post.author.viewer?.blocking || post.author.viewer?.blockedBy) { - return { - $type: 'app.bsky.embed.record#viewBlocked', - uri, +} + +const postRecordsFromInfos = ( + infos: PostInfoMap, +): { [uri: string]: PostRecord } => { + const records: { [uri: string]: PostRecord } = {} + for (const [uri, info] of Object.entries(infos)) { + const record = jsonStringToLex(info.recordJson) + if (isPostRecord(record)) { + records[uri] = record } } - return { - $type: 'app.bsky.embed.record#viewRecord', - uri: post.uri, - cid: post.cid, - author: post.author, - value: post.record, - labels: post.labels, - indexedAt: post.indexedAt, - embeds, + return records +} + +const nestedRecordUris = (posts: PostRecord[]): string[] => { + const uris: string[] = [] + for (const post of posts) { + if (!post.embed) continue + if (isEmbedRecord(post.embed)) { + uris.push(post.embed.record.uri) + } else if (isEmbedRecordWithMedia(post.embed)) { + uris.push(post.embed.record.record.uri) + } else { + continue + } } + return uris } diff --git a/packages/bsky/src/services/feed/types.ts b/packages/bsky/src/services/feed/types.ts index 983d7403683..d133b3634c2 100644 --- a/packages/bsky/src/services/feed/types.ts +++ b/packages/bsky/src/services/feed/types.ts @@ -1,16 +1,97 @@ -import { Selectable } from 'kysely' +import { View as ImagesEmbedView } from '../../lexicon/types/app/bsky/embed/images' +import { View as ExternalEmbedView } from '../../lexicon/types/app/bsky/embed/external' +import { + ViewBlocked, + ViewNotFound, + ViewRecord, + View as RecordEmbedView, +} from '../../lexicon/types/app/bsky/embed/record' +import { View as RecordWithMediaEmbedView } from '../../lexicon/types/app/bsky/embed/recordWithMedia' import { BlockedPost, + GeneratorView, NotFoundPost, PostView, } from '../../lexicon/types/app/bsky/feed/defs' +import { Label } from '../../lexicon/types/com/atproto/label/defs' import { FeedGenerator } from '../../db/tables/feed-generator' +import { ListView } from '../../lexicon/types/app/bsky/graph/defs' +import { Selectable } from 'kysely' + +export type PostEmbedViews = { + [uri: string]: PostEmbedView +} + +export type PostEmbedView = + | ImagesEmbedView + | ExternalEmbedView + | RecordEmbedView + | RecordWithMediaEmbedView + +export type PostViews = { [uri: string]: PostView } + +export type PostInfo = { + uri: string + cid: string + creator: string + recordJson: string + indexedAt: string + likeCount: number | null + repostCount: number | null + replyCount: number | null + requesterRepost: string | null + requesterLike: string | null + viewer: string | null +} + +export type PostInfoMap = { [uri: string]: PostInfo } + +export type ActorInfo = { + did: string + handle: string + displayName?: string + avatar?: string + viewer?: { + muted?: boolean + blockedBy?: boolean + blocking?: string + following?: string + followedBy?: string + } + labels?: Label[] +} +export type ActorInfoMap = { [did: string]: ActorInfo } export type FeedGenInfo = Selectable & { likeCount: number - viewerLike: string | null + viewer?: { + like?: string + } } export type FeedGenInfoMap = { [uri: string]: FeedGenInfo } +export type FeedItemType = 'post' | 'repost' + +export type FeedRow = { + type: FeedItemType + uri: string + cid: string + postUri: string + postAuthorDid: string + originatorDid: string + replyParent: string | null + replyRoot: string | null + sortAt: string +} + export type MaybePostView = PostView | NotFoundPost | BlockedPost + +export type RecordEmbedViewRecord = + | ViewRecord + | ViewNotFound + | ViewBlocked + | GeneratorView + | ListView + +export type RecordEmbedViewRecordMap = { [uri: string]: RecordEmbedViewRecord } diff --git a/packages/bsky/src/services/feed/views.ts b/packages/bsky/src/services/feed/views.ts index 99d0c95cf20..ceee0838783 100644 --- a/packages/bsky/src/services/feed/views.ts +++ b/packages/bsky/src/services/feed/views.ts @@ -1,15 +1,38 @@ +import { LexRecord, jsonStringToLex } from '@atproto/lexicon' import Database from '../../db' import { FeedViewPost, GeneratorView, PostView, } from '../../lexicon/types/app/bsky/feed/defs' -import { ProfileView } from '../../lexicon/types/app/bsky/actor/defs' -import { FeedGenInfo, MaybePostView } from './types' +import { + Main as EmbedImages, + isMain as isEmbedImages, + View as EmbedImagesView, +} from '../../lexicon/types/app/bsky/embed/images' +import { + Main as EmbedExternal, + isMain as isEmbedExternal, + View as EmbedExternalView, +} from '../../lexicon/types/app/bsky/embed/external' +import { Main as EmbedRecordWithMedia } from '../../lexicon/types/app/bsky/embed/recordWithMedia' +import { + ViewBlocked, + ViewNotFound, + ViewRecord, +} from '../../lexicon/types/app/bsky/embed/record' +import { + ActorInfoMap, + PostEmbedViews, + FeedGenInfo, + FeedRow, + MaybePostView, + PostInfoMap, + RecordEmbedViewRecord, +} from './types' import { Labels } from '../label' +import { ProfileView } from '../../lexicon/types/app/bsky/actor/defs' import { ImageUriBuilder } from '../../image/uri' -import { ActorViewMap, FeedEmbeds, FeedRow, PostInfoMap } from '../types' -import { jsonStringToLex } from '@atproto/lexicon' export class FeedViews { constructor(public db: Database, public imgUriBuilder: ImageUriBuilder) {} @@ -18,44 +41,49 @@ export class FeedViews { return (db: Database) => new FeedViews(db, imgUriBuilder) } - formatPostView( - uri: string, - actors: ActorViewMap, - posts: PostInfoMap, - embeds: FeedEmbeds, - labels: Labels, - ): PostView | undefined { - const post = posts[uri] - const author = actors[post?.creator] - if (!post || !author) return undefined - // If the author labels are not hydrated yet, attempt to pull them - // from labels: e.g. compatible with hydrateFeed() batching label hydration. - author.labels ??= labels[author.did] ?? [] + formatFeedGeneratorView( + info: FeedGenInfo, + profiles: Record, + labels?: Labels, + ): GeneratorView { + const profile = profiles[info.creator] + if (profile) { + // If the creator labels are not hydrated yet, attempt to pull them + // from labels: e.g. compatible with embedsForPosts() batching label hydration. + profile.labels ??= labels?.[info.creator] ?? [] + } return { - uri: post.uri, - cid: post.cid, - author: author, - record: jsonStringToLex(post.recordJson) as Record, - embed: embeds[uri], - replyCount: post.replyCount ?? 0, - repostCount: post.repostCount ?? 0, - likeCount: post.likeCount ?? 0, - indexedAt: post.indexedAt, - viewer: post.viewer + uri: info.uri, + cid: info.cid, + did: info.feedDid, + creator: profile, + displayName: info.displayName ?? undefined, + description: info.description ?? undefined, + descriptionFacets: info.descriptionFacets + ? JSON.parse(info.descriptionFacets) + : undefined, + avatar: info.avatarCid + ? this.imgUriBuilder.getCommonSignedUri( + 'avatar', + info.creator, + info.avatarCid, + ) + : undefined, + likeCount: info.likeCount, + viewer: info.viewer ? { - repost: post.requesterRepost ?? undefined, - like: post.requesterLike ?? undefined, + like: info.viewer.like ?? undefined, } : undefined, - labels: labels[uri] ?? [], + indexedAt: info.indexedAt, } } formatFeed( items: FeedRow[], - actors: ActorViewMap, + actors: ActorInfoMap, posts: PostInfoMap, - embeds: FeedEmbeds, + embeds: PostEmbedViews, labels: Labels, usePostViewUnion?: boolean, ): FeedViewPost[] { @@ -118,48 +146,44 @@ export class FeedViews { return feed } - formatFeedGeneratorView( - info: FeedGenInfo, - profiles: Record, - labels?: Labels, - ): GeneratorView { - const profile = profiles[info.creator] - if (profile) { - // If the creator labels are not hydrated yet, attempt to pull them - // from labels: e.g. compatible with embedsForPosts() batching label hydration. - profile.labels ??= labels?.[info.creator] ?? [] - } + formatPostView( + uri: string, + actors: ActorInfoMap, + posts: PostInfoMap, + embeds: PostEmbedViews, + labels: Labels, + ): PostView | undefined { + const post = posts[uri] + const author = actors[post?.creator] + if (!post || !author) return undefined + // If the author labels are not hydrated yet, attempt to pull them + // from labels: e.g. compatible with hydrateFeed() batching label hydration. + author.labels ??= labels[author.did] ?? [] return { - uri: info.uri, - cid: info.cid, - did: info.feedDid, - creator: profile, - displayName: info.displayName ?? undefined, - description: info.description ?? undefined, - descriptionFacets: info.descriptionFacets - ? JSON.parse(info.descriptionFacets) - : undefined, - avatar: info.avatarCid - ? this.imgUriBuilder.getCommonSignedUri( - 'avatar', - info.creator, - info.avatarCid, - ) + uri: post.uri, + cid: post.cid, + author: author, + record: jsonStringToLex(post.recordJson) as LexRecord, + embed: embeds[uri], + replyCount: post.replyCount ?? 0, + repostCount: post.repostCount ?? 0, + likeCount: post.likeCount ?? 0, + indexedAt: post.indexedAt, + viewer: post.viewer + ? { + repost: post.requesterRepost ?? undefined, + like: post.requesterLike ?? undefined, + } : undefined, - likeCount: info.likeCount, - viewer: { - like: info.viewerLike ?? undefined, - }, - indexedAt: info.sortAt, + labels: labels[uri] ?? [], } } - // @TODO present block here (w/ usePostViewUnion) formatMaybePostView( uri: string, - actors: ActorViewMap, + actors: ActorInfoMap, posts: PostInfoMap, - embeds: FeedEmbeds, + embeds: PostEmbedViews, labels: Labels, usePostViewUnion?: boolean, ): MaybePostView | undefined { @@ -193,4 +217,94 @@ export class FeedViews { notFound: true as const, } } + + imagesEmbedView(did: string, embed: EmbedImages) { + const imgViews = embed.images.map((img) => ({ + thumb: this.imgUriBuilder.getCommonSignedUri( + 'feed_thumbnail', + did, + img.image.ref, + ), + fullsize: this.imgUriBuilder.getCommonSignedUri( + 'feed_fullsize', + did, + img.image.ref, + ), + alt: img.alt, + })) + return { + $type: 'app.bsky.embed.images#view', + images: imgViews, + } + } + + externalEmbedView(did: string, embed: EmbedExternal) { + const { uri, title, description, thumb } = embed.external + return { + $type: 'app.bsky.embed.external#view', + external: { + uri, + title, + description, + thumb: thumb + ? this.imgUriBuilder.getCommonSignedUri( + 'feed_thumbnail', + did, + thumb.ref, + ) + : undefined, + }, + } + } + + getRecordEmbedView( + uri: string, + post?: PostView, + omitEmbeds = false, + ): (ViewRecord | ViewNotFound | ViewBlocked) & { $type: string } { + if (!post) { + return { + $type: 'app.bsky.embed.record#viewNotFound', + uri, + } + } + if (post.author.viewer?.blocking || post.author.viewer?.blockedBy) { + return { + $type: 'app.bsky.embed.record#viewBlocked', + uri, + } + } + return { + $type: 'app.bsky.embed.record#viewRecord', + uri: post.uri, + cid: post.cid, + author: post.author, + value: post.record, + labels: post.labels, + indexedAt: post.indexedAt, + embeds: omitEmbeds ? undefined : post.embed ? [post.embed] : [], + } + } + + getRecordWithMediaEmbedView( + did: string, + embed: EmbedRecordWithMedia, + embedRecordView: RecordEmbedViewRecord, + ) { + let mediaEmbed: EmbedImagesView | EmbedExternalView + if (isEmbedImages(embed.media)) { + mediaEmbed = this.imagesEmbedView(did, embed.media) + } else if (isEmbedExternal(embed.media)) { + mediaEmbed = this.externalEmbedView(did, embed.media) + } else { + return + } + return { + $type: 'app.bsky.embed.recordWithMedia#view', + record: { + record: embedRecordView, + }, + media: mediaEmbed, + } + } } diff --git a/packages/bsky/src/services/graph/index.ts b/packages/bsky/src/services/graph/index.ts index 8abdfe6ce0b..22e2c5b4fd5 100644 --- a/packages/bsky/src/services/graph/index.ts +++ b/packages/bsky/src/services/graph/index.ts @@ -211,6 +211,26 @@ export class GraphService { }, } } + + formatListViewBasic(list: ListInfo) { + return { + uri: list.uri, + cid: list.cid, + name: list.name, + purpose: list.purpose, + avatar: list.avatarCid + ? this.imgUriBuilder.getCommonSignedUri( + 'avatar', + list.creator, + list.avatarCid, + ) + : undefined, + indexedAt: list.indexedAt, + viewer: { + muted: !!list.viewerMuted, + }, + } + } } type ListInfo = Selectable & { diff --git a/packages/bsky/src/services/types.ts b/packages/bsky/src/services/types.ts deleted file mode 100644 index d29c1c44c17..00000000000 --- a/packages/bsky/src/services/types.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { View as ViewImages } from '../lexicon/types/app/bsky/embed/images' -import { View as ViewExternal } from '../lexicon/types/app/bsky/embed/external' -import { View as ViewRecord } from '../lexicon/types/app/bsky/embed/record' -import { View as ViewRecordWithMedia } from '../lexicon/types/app/bsky/embed/recordWithMedia' -import { Label } from '../lexicon/types/com/atproto/label/defs' - -export type FeedEmbeds = { - [uri: string]: ViewImages | ViewExternal | ViewRecord | ViewRecordWithMedia -} - -export type PostInfo = { - uri: string - cid: string - creator: string - recordJson: string - indexedAt: string - likeCount: number | null - repostCount: number | null - replyCount: number | null - requesterRepost: string | null - requesterLike: string | null - viewer: string | null -} - -export type PostInfoMap = { [uri: string]: PostInfo } - -export type ActorView = { - did: string - handle: string - displayName?: string - avatar?: string - viewer?: { muted?: boolean; following?: string; followedBy?: string } - labels?: Label[] -} -export type ActorViewMap = { [did: string]: ActorView } - -export type FeedItemType = 'post' | 'repost' - -export type FeedRow = { - type: FeedItemType - uri: string - cid: string - postUri: string - postAuthorDid: string - originatorDid: string - replyParent: string | null - replyRoot: string | null - sortAt: string -} From 353ff19f995fcab103b1d997abac407426f91db2 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Mon, 24 Jul 2023 16:35:04 -0400 Subject: [PATCH 061/237] Apply block rules for third parties on replies and embeds (#1378) * apply block rules for third parties on replies and embeds * tidy terminology around blocking and actor relationships * apply third-party blocking functionality to appview * tidy, bsky tests --- .../src/api/app/bsky/feed/getPostThread.ts | 10 +- packages/bsky/src/services/feed/index.ts | 167 +++++++++++++++-- packages/bsky/src/services/feed/types.ts | 6 +- packages/bsky/src/services/feed/views.ts | 16 +- packages/bsky/tests/views/blocks.test.ts | 162 ++++++++++++++++- .../api/app/bsky/feed/getPostThread.ts | 10 +- .../pds/src/app-view/services/feed/index.ts | 170 ++++++++++++++++-- .../pds/src/app-view/services/feed/types.ts | 7 +- .../pds/src/app-view/services/feed/views.ts | 16 +- packages/pds/tests/views/blocks.test.ts | 161 +++++++++++++++-- 10 files changed, 659 insertions(+), 66 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts index 351fc30fe56..2448cc131a4 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts @@ -5,6 +5,7 @@ import { FeedRow, ActorInfoMap, PostEmbedViews, + PostBlocksMap, } from '../../../../services/feed/types' import { FeedService, PostInfoMap } from '../../../../services/feed' import { Labels } from '../../../../services/label' @@ -47,7 +48,8 @@ export default function (server: Server, ctx: AppContext) { feedService.getPostInfos(Array.from(relevant.uris), requester), labelService.getLabelsForSubjects([...relevant.uris, ...relevant.dids]), ]) - const embeds = await feedService.embedsForPosts(posts, requester) + const blocks = await feedService.blocksForPosts(posts) + const embeds = await feedService.embedsForPosts(posts, blocks, requester) const thread = composeThread( threadData, @@ -55,6 +57,7 @@ export default function (server: Server, ctx: AppContext) { posts, actors, embeds, + blocks, labels, ) @@ -77,6 +80,7 @@ const composeThread = ( posts: PostInfoMap, actors: ActorInfoMap, embeds: PostEmbedViews, + blocks: PostBlocksMap, labels: Labels, ) => { const post = feedService.views.formatPostView( @@ -87,7 +91,7 @@ const composeThread = ( labels, ) - if (!post) { + if (!post || blocks[post.uri]?.reply) { return { $type: 'app.bsky.feed.defs#notFoundPost', uri: threadData.post.postUri, @@ -118,6 +122,7 @@ const composeThread = ( posts, actors, embeds, + blocks, labels, ) } @@ -132,6 +137,7 @@ const composeThread = ( posts, actors, embeds, + blocks, labels, ) // e.g. don't bother including #postNotFound reply placeholders for takedowns. either way matches api contract. diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index 81d3937ba4e..c2156d2ca33 100644 --- a/packages/bsky/src/services/feed/index.ts +++ b/packages/bsky/src/services/feed/index.ts @@ -4,7 +4,12 @@ import { dedupeStrs } from '@atproto/common' import { INVALID_HANDLE } from '@atproto/identifier' import { jsonStringToLex } from '@atproto/lexicon' import Database from '../../db' -import { countAll, noMatch, notSoftDeletedClause } from '../../db/util' +import { + countAll, + noMatch, + notSoftDeletedClause, + valuesList, +} from '../../db/util' import { ImageUriBuilder } from '../../image/uri' import { ids } from '../../lexicon/lexicons' import { @@ -13,7 +18,10 @@ import { } from '../../lexicon/types/app/bsky/feed/post' import { isMain as isEmbedImages } from '../../lexicon/types/app/bsky/embed/images' import { isMain as isEmbedExternal } from '../../lexicon/types/app/bsky/embed/external' -import { isMain as isEmbedRecord } from '../../lexicon/types/app/bsky/embed/record' +import { + isMain as isEmbedRecord, + isViewRecord, +} from '../../lexicon/types/app/bsky/embed/record' import { isMain as isEmbedRecordWithMedia } from '../../lexicon/types/app/bsky/embed/recordWithMedia' import { FeedViewPost } from '../../lexicon/types/app/bsky/feed/defs' import { @@ -25,6 +33,9 @@ import { PostViews, PostEmbedViews, RecordEmbedViewRecordMap, + PostInfo, + RecordEmbedViewRecord, + PostBlocksMap, } from './types' import { LabelService, Labels } from '../label' import { ActorService } from '../actor' @@ -253,13 +264,15 @@ export class FeedService { .as('requesterLike'), ]) .execute() - return posts.reduce( - (acc, cur) => ({ - ...acc, - [cur.uri]: Object.assign(cur, { viewer }), - }), - {} as PostInfoMap, - ) + return posts.reduce((acc, cur) => { + const { recordJson, ...post } = cur + const info: PostInfo = { + ...post, + record: jsonStringToLex(recordJson) as Record, + viewer, + } + return Object.assign(acc, { [post.uri]: info }) + }, {} as PostInfoMap) } async getFeedGeneratorInfos(generatorUris: string[], viewer: string | null) { @@ -286,6 +299,7 @@ export class FeedService { actors?: ActorInfoMap posts?: PostInfoMap embeds?: PostEmbedViews + blocks?: PostBlocksMap labels?: Labels }, ): Promise { @@ -299,8 +313,10 @@ export class FeedService { precomputed?.labels ?? this.services.label.getLabelsForSubjects([...uris, ...dids]), ]) + const blocks = precomputed?.blocks ?? (await this.blocksForPosts(posts)) const embeds = - precomputed?.embeds ?? (await this.embedsForPosts(posts, requester)) + precomputed?.embeds ?? + (await this.embedsForPosts(posts, blocks, requester)) return uris.reduce((acc, cur) => { const view = this.views.formatPostView(cur, actors, posts, embeds, labels) @@ -341,7 +357,8 @@ export class FeedService { this.getPostInfos(Array.from(postUris), viewer), this.services.label.getLabelsForSubjects([...postUris, ...actorDids]), ]) - const embeds = await this.embedsForPosts(posts, viewer) + const blocks = await this.blocksForPosts(posts) + const embeds = await this.embedsForPosts(posts, blocks, viewer) return this.views.formatFeed( items, @@ -349,12 +366,80 @@ export class FeedService { posts, embeds, labels, + blocks, usePostViewUnion, ) } + // applies blocks for visibility to third-parties (i.e. based on post content) + async blocksForPosts(posts: PostInfoMap): Promise { + const relationships: RelationshipPair[] = [] + const byPost: Record = {} + const didFromUri = (uri) => new AtUri(uri).host + for (const post of Object.values(posts)) { + // skip posts that we can't process or appear to already have been processed + if (!isPostRecord(post.record)) continue + if (byPost[post.uri]) continue + byPost[post.uri] = {} + // 3p block for replies + const parentUri = post.record.reply?.parent.uri + const parentDid = parentUri ? didFromUri(parentUri) : null + // 3p block for record embeds + const embedUris = nestedRecordUris([post.record]) + // gather actor relationships among posts + if (parentDid) { + const pair: RelationshipPair = [post.creator, parentDid] + relationships.push(pair) + byPost[post.uri].reply = pair + } + for (const embedUri of embedUris) { + const pair: RelationshipPair = [post.creator, didFromUri(embedUri)] + relationships.push(pair) + byPost[post.uri].embed = pair + } + } + // compute block state from all actor relationships among posts + const blockSet = await this.getBlockSet(relationships) + if (blockSet.empty()) return {} + const result: PostBlocksMap = {} + Object.entries(byPost).forEach(([uri, block]) => { + if (block.embed && blockSet.has(block.embed)) { + result[uri] ??= {} + result[uri].embed = true + } + if (block.reply && blockSet.has(block.reply)) { + result[uri] ??= {} + result[uri].reply = true + } + }) + return result + } + + private async getBlockSet(relationships: RelationshipPair[]) { + const { ref } = this.db.db.dynamic + const blockSet = new RelationshipSet() + if (!relationships.length) return blockSet + const relationshipSet = new RelationshipSet() + relationships.forEach((pair) => relationshipSet.add(pair)) + // compute actual block set from all actor relationships + const blockRows = await this.db.db + .selectFrom('actor_block') + .select(['creator', 'subjectDid']) // index-only columns + .where( + sql`(${ref('creator')}, ${ref('subjectDid')})`, + 'in', + valuesList( + relationshipSet.listAllPairs().map(([a, b]) => sql`${a}, ${b}`), + ), + ) + .execute() + blockRows.forEach((r) => blockSet.add([r.creator, r.subjectDid])) + return blockSet + } + async embedsForPosts( postInfos: PostInfoMap, + blocks: PostBlocksMap, viewer: string | null, depth = 0, ) { @@ -378,7 +463,11 @@ export class FeedService { if (!recordEmbedViews[post.embed.record.uri]) continue postEmbedViews[uri] = { $type: 'app.bsky.embed.record#view', - record: recordEmbedViews[post.embed.record.uri], + record: applyEmbedBlock( + uri, + blocks, + recordEmbedViews[post.embed.record.uri], + ), } } else if (isEmbedRecordWithMedia(post.embed)) { const embedRecordView = recordEmbedViews[post.embed.record.record.uri] @@ -386,7 +475,7 @@ export class FeedService { const formatted = this.views.getRecordWithMediaEmbedView( creator, post.embed, - embedRecordView, + applyEmbedBlock(uri, blocks, embedRecordView), ) if (formatted) { postEmbedViews[uri] = formatted @@ -430,8 +519,10 @@ export class FeedService { this.getFeedGeneratorInfos(nestedFeedGenUris, viewer), this.services.graph.getListViews(nestedListUris, viewer), ]) + const deepBlocks = await this.blocksForPosts(postInfos) const deepEmbedViews = await this.embedsForPosts( postInfos, + deepBlocks, viewer, depth + 1, ) @@ -481,9 +572,8 @@ const postRecordsFromInfos = ( ): { [uri: string]: PostRecord } => { const records: { [uri: string]: PostRecord } = {} for (const [uri, info] of Object.entries(infos)) { - const record = jsonStringToLex(info.recordJson) - if (isPostRecord(record)) { - records[uri] = record + if (isPostRecord(info.record)) { + records[uri] = info.record } } return records @@ -503,3 +593,48 @@ const nestedRecordUris = (posts: PostRecord[]): string[] => { } return uris } + +type PostRelationships = { reply?: RelationshipPair; embed?: RelationshipPair } + +type RelationshipPair = [didA: string, didB: string] + +class RelationshipSet { + index = new Map>() + add([didA, didB]: RelationshipPair) { + const didAIdx = this.index.get(didA) ?? new Set() + const didBIdx = this.index.get(didB) ?? new Set() + if (!this.index.has(didA)) this.index.set(didA, didAIdx) + if (!this.index.has(didB)) this.index.set(didB, didBIdx) + didAIdx.add(didB) + didBIdx.add(didA) + } + has([didA, didB]: RelationshipPair) { + return !!this.index.get(didA)?.has(didB) + } + listAllPairs() { + const pairs: RelationshipPair[] = [] + for (const [didA, didBIdx] of this.index.entries()) { + for (const didB of didBIdx) { + pairs.push([didA, didB]) + } + } + return pairs + } + empty() { + return this.index.size === 0 + } +} + +function applyEmbedBlock( + uri: string, + blocks: PostBlocksMap, + view: RecordEmbedViewRecord, +): RecordEmbedViewRecord { + if (isViewRecord(view) && blocks[uri]?.embed) { + return { + $type: 'app.bsky.embed.record#viewBlocked', + uri: view.uri, + } + } + return view +} diff --git a/packages/bsky/src/services/feed/types.ts b/packages/bsky/src/services/feed/types.ts index d133b3634c2..b32f2a646be 100644 --- a/packages/bsky/src/services/feed/types.ts +++ b/packages/bsky/src/services/feed/types.ts @@ -34,7 +34,7 @@ export type PostInfo = { uri: string cid: string creator: string - recordJson: string + record: Record indexedAt: string likeCount: number | null repostCount: number | null @@ -46,6 +46,10 @@ export type PostInfo = { export type PostInfoMap = { [uri: string]: PostInfo } +export type PostBlocksMap = { + [uri: string]: { reply?: boolean; embed?: boolean } +} + export type ActorInfo = { did: string handle: string diff --git a/packages/bsky/src/services/feed/views.ts b/packages/bsky/src/services/feed/views.ts index ceee0838783..fe986fe9a54 100644 --- a/packages/bsky/src/services/feed/views.ts +++ b/packages/bsky/src/services/feed/views.ts @@ -1,4 +1,3 @@ -import { LexRecord, jsonStringToLex } from '@atproto/lexicon' import Database from '../../db' import { FeedViewPost, @@ -29,6 +28,7 @@ import { MaybePostView, PostInfoMap, RecordEmbedViewRecord, + PostBlocksMap, } from './types' import { Labels } from '../label' import { ProfileView } from '../../lexicon/types/app/bsky/actor/defs' @@ -85,6 +85,7 @@ export class FeedViews { posts: PostInfoMap, embeds: PostEmbedViews, labels: Labels, + blocks: PostBlocksMap, usePostViewUnion?: boolean, ): FeedViewPost[] { const feed: FeedViewPost[] = [] @@ -97,7 +98,7 @@ export class FeedViews { labels, ) // skip over not found & blocked posts - if (!post) { + if (!post || blocks[post.uri]?.reply) { continue } const feedPost = { post } @@ -124,6 +125,7 @@ export class FeedViews { posts, embeds, labels, + blocks, usePostViewUnion, ) const replyRoot = this.formatMaybePostView( @@ -132,6 +134,7 @@ export class FeedViews { posts, embeds, labels, + blocks, usePostViewUnion, ) if (replyRoot && replyParent) { @@ -163,7 +166,7 @@ export class FeedViews { uri: post.uri, cid: post.cid, author: author, - record: jsonStringToLex(post.recordJson) as LexRecord, + record: post.record, embed: embeds[uri], replyCount: post.replyCount ?? 0, repostCount: post.repostCount ?? 0, @@ -185,6 +188,7 @@ export class FeedViews { posts: PostInfoMap, embeds: PostEmbedViews, labels: Labels, + blocks: PostBlocksMap, usePostViewUnion?: boolean, ): MaybePostView | undefined { const post = this.formatPostView(uri, actors, posts, embeds, labels) @@ -192,7 +196,11 @@ export class FeedViews { if (!usePostViewUnion) return return this.notFoundPost(uri) } - if (post.author.viewer?.blockedBy || post.author.viewer?.blocking) { + if ( + post.author.viewer?.blockedBy || + post.author.viewer?.blocking || + blocks[uri]?.reply + ) { if (!usePostViewUnion) return return this.blockedPost(uri) } diff --git a/packages/bsky/tests/views/blocks.test.ts b/packages/bsky/tests/views/blocks.test.ts index 400b74823e0..3e605cb11f6 100644 --- a/packages/bsky/tests/views/blocks.test.ts +++ b/packages/bsky/tests/views/blocks.test.ts @@ -1,6 +1,12 @@ -import AtpAgent from '@atproto/api' +import assert from 'assert' +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' +import { isThreadViewPost } from '@atproto/api/src/client/types/app/bsky/feed/defs' +import { + isViewRecord as isEmbedViewRecord, + isViewBlocked as isEmbedViewBlocked, +} from '@atproto/api/src/client/types/app/bsky/embed/record' import { TestNetwork } from '@atproto/dev-env' import { forSnapshot } from '../_util' import { RecordRef, SeedClient } from '../seeds/client' @@ -11,7 +17,9 @@ describe('pds views with blocking', () => { let agent: AtpAgent let pdsAgent: AtpAgent let sc: SeedClient + let danBlockCarol: { uri: string } let aliceReplyToDan: { ref: RecordRef } + let carolReplyToDan: { ref: RecordRef } let alice: string let carol: string @@ -31,18 +39,24 @@ describe('pds views with blocking', () => { // add follows to ensure blocks work even w follows await sc.follow(carol, dan) await sc.follow(dan, carol) - // dan blocks carol - await pdsAgent.api.app.bsky.graph.block.create( - { repo: dan }, - { createdAt: new Date().toISOString(), subject: carol }, - sc.getHeaders(dan), - ) aliceReplyToDan = await sc.reply( alice, sc.posts[dan][0].ref, sc.posts[dan][0].ref, 'alice replies to dan', ) + carolReplyToDan = await sc.reply( + carol, + sc.posts[dan][0].ref, + sc.posts[dan][0].ref, + 'carol replies to dan', + ) + // dan blocks carol + danBlockCarol = await pdsAgent.api.app.bsky.graph.block.create( + { repo: dan }, + { createdAt: new Date().toISOString(), subject: carol }, + sc.getHeaders(dan), + ) await network.processAll() }) @@ -51,7 +65,6 @@ describe('pds views with blocking', () => { }) it('blocks thread post', async () => { - const { carol, dan } = sc.dids const { data: threadAlice } = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.posts[carol][0].ref.uriStr }, { headers: await network.serviceHeaders(dan) }, @@ -233,6 +246,7 @@ describe('pds views with blocking', () => { // unfollow so they _would_ show up in suggestions if not for block await sc.unfollow(carol, dan) await sc.unfollow(dan, carol) + await network.processAll() const resCarol = await agent.api.app.bsky.actor.getSuggestions( { @@ -251,6 +265,138 @@ describe('pds views with blocking', () => { expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy() }) + it('does not serve blocked replies', async () => { + const getThreadPostUri = (r) => r?.['post']?.['uri'] + // reply then block + const { data: replyThenBlock } = + await agent.api.app.bsky.feed.getPostThread( + { depth: 1, uri: sc.posts[dan][0].ref.uriStr }, + { headers: await network.serviceHeaders(alice) }, + ) + assert(isThreadViewPost(replyThenBlock.thread)) + expect(replyThenBlock.thread.replies?.map(getThreadPostUri)).toEqual([ + aliceReplyToDan.ref.uriStr, + ]) + + // unblock + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: dan, rkey: new AtUri(danBlockCarol.uri).rkey }, + sc.getHeaders(dan), + ) + await network.processAll() + const { data: unblock } = await agent.api.app.bsky.feed.getPostThread( + { depth: 1, uri: sc.posts[dan][0].ref.uriStr }, + { headers: await network.serviceHeaders(alice) }, + ) + assert(isThreadViewPost(unblock.thread)) + expect(unblock.thread.replies?.map(getThreadPostUri)).toEqual([ + carolReplyToDan.ref.uriStr, + aliceReplyToDan.ref.uriStr, + ]) + + // block then reply + danBlockCarol = await pdsAgent.api.app.bsky.graph.block.create( + { repo: dan }, + { createdAt: new Date().toISOString(), subject: carol }, + sc.getHeaders(dan), + ) + const carolReplyToDan2 = await sc.reply( + carol, + sc.posts[dan][1].ref, + sc.posts[dan][1].ref, + 'carol replies to dan again', + ) + await network.processAll() + const { data: blockThenReply } = + await agent.api.app.bsky.feed.getPostThread( + { depth: 1, uri: sc.posts[dan][0].ref.uriStr }, + { headers: await network.serviceHeaders(alice) }, + ) + assert(isThreadViewPost(blockThenReply.thread)) + expect(replyThenBlock.thread.replies?.map(getThreadPostUri)).toEqual([ + aliceReplyToDan.ref.uriStr, + ]) + + // cleanup + await pdsAgent.api.app.bsky.feed.post.delete( + { repo: carol, rkey: carolReplyToDan2.ref.uri.rkey }, + sc.getHeaders(carol), + ) + await network.processAll() + }) + + it('does not serve blocked embeds to third-party', async () => { + // embed then block + const { data: embedThenBlock } = + await agent.api.app.bsky.feed.getPostThread( + { depth: 0, uri: sc.posts[dan][1].ref.uriStr }, + { headers: await network.serviceHeaders(alice) }, + ) + assert(isThreadViewPost(embedThenBlock.thread)) + assert(isEmbedViewBlocked(embedThenBlock.thread.post.embed?.record)) + + // unblock + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: dan, rkey: new AtUri(danBlockCarol.uri).rkey }, + sc.getHeaders(dan), + ) + await network.processAll() + const { data: unblock } = await agent.api.app.bsky.feed.getPostThread( + { depth: 0, uri: sc.posts[dan][1].ref.uriStr }, + { headers: await network.serviceHeaders(alice) }, + ) + assert(isThreadViewPost(unblock.thread)) + assert(isEmbedViewRecord(unblock.thread.post.embed?.record)) + + // block then embed + danBlockCarol = await pdsAgent.api.app.bsky.graph.block.create( + { repo: dan }, + { createdAt: new Date().toISOString(), subject: carol }, + sc.getHeaders(dan), + ) + const carolEmbedsDan = await sc.post( + carol, + 'carol embeds dan', + undefined, + undefined, + sc.posts[dan][0].ref, + ) + await network.processAll() + const { data: blockThenEmbed } = + await agent.api.app.bsky.feed.getPostThread( + { depth: 0, uri: carolEmbedsDan.ref.uriStr }, + { headers: await network.serviceHeaders(alice) }, + ) + assert(isThreadViewPost(blockThenEmbed.thread)) + assert(isEmbedViewBlocked(blockThenEmbed.thread.post.embed?.record)) + + // cleanup + await pdsAgent.api.app.bsky.feed.post.delete( + { repo: carol, rkey: carolEmbedsDan.ref.uri.rkey }, + sc.getHeaders(carol), + ) + await network.processAll() + }) + + it('applies third-party blocking rules in feeds.', async () => { + // alice follows carol and dan, block exists between carol and dan. + const replyBlockedUri = carolReplyToDan.ref.uriStr + const embedBlockedUri = sc.posts[dan][1].ref.uriStr + const { data: timeline } = await agent.api.app.bsky.feed.getTimeline( + { limit: 100 }, + { headers: await network.serviceHeaders(alice) }, + ) + const replyBlockedPost = timeline.feed.find( + (item) => item.post.uri === replyBlockedUri, + ) + expect(replyBlockedPost).toBeUndefined() + const embedBlockedPost = timeline.feed.find( + (item) => item.post.uri === embedBlockedUri, + ) + assert(embedBlockedPost) + assert(isEmbedViewBlocked(embedBlockedPost.post.embed?.record)) + }) + it('returns a list of blocks', async () => { await pdsAgent.api.app.bsky.graph.block.create( { repo: dan }, diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts index da8f72dfde2..7d0478e1432 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts @@ -7,6 +7,7 @@ import { FeedRow, FeedService, PostInfoMap, + PostBlocksMap, } from '../../../../services/feed' import { Labels } from '../../../../services/label' import { @@ -60,7 +61,8 @@ export default function (server: Server, ctx: AppContext) { feedService.getPostInfos(Array.from(relevant.uris), requester), labelService.getLabelsForSubjects([...relevant.uris, ...relevant.dids]), ]) - const embeds = await feedService.embedsForPosts(posts, requester) + const blocks = await feedService.blocksForPosts(posts) + const embeds = await feedService.embedsForPosts(posts, blocks, requester) const thread = composeThread( threadData, @@ -68,6 +70,7 @@ export default function (server: Server, ctx: AppContext) { posts, actors, embeds, + blocks, labels, ) @@ -90,6 +93,7 @@ const composeThread = ( posts: PostInfoMap, actors: ActorInfoMap, embeds: PostEmbedViews, + blocks: PostBlocksMap, labels: Labels, ): ThreadViewPost | NotFoundPost | BlockedPost => { const post = feedService.views.formatPostView( @@ -100,7 +104,7 @@ const composeThread = ( labels, ) - if (!post) { + if (!post || blocks[post.uri]?.reply) { return { $type: 'app.bsky.feed.defs#notFoundPost', uri: threadData.post.postUri, @@ -131,6 +135,7 @@ const composeThread = ( posts, actors, embeds, + blocks, labels, ) } @@ -145,6 +150,7 @@ const composeThread = ( posts, actors, embeds, + blocks, labels, ) // e.g. don't bother including #postNotFound reply placeholders for takedowns. either way matches api contract. diff --git a/packages/pds/src/app-view/services/feed/index.ts b/packages/pds/src/app-view/services/feed/index.ts index af4d41590a6..3f1fc54a799 100644 --- a/packages/pds/src/app-view/services/feed/index.ts +++ b/packages/pds/src/app-view/services/feed/index.ts @@ -3,7 +3,7 @@ import { AtUri } from '@atproto/uri' import { dedupeStrs } from '@atproto/common' import { cborToLexRecord } from '@atproto/repo' import Database from '../../../db' -import { countAll, notSoftDeletedClause } from '../../../db/util' +import { countAll, notSoftDeletedClause, valuesList } from '../../../db/util' import { ImageUriBuilder } from '../../../image/uri' import { ids } from '../../../lexicon/lexicons' import { @@ -12,7 +12,10 @@ import { } from '../../../lexicon/types/app/bsky/feed/post' import { isMain as isEmbedImages } from '../../../lexicon/types/app/bsky/embed/images' import { isMain as isEmbedExternal } from '../../../lexicon/types/app/bsky/embed/external' -import { isMain as isEmbedRecord } from '../../../lexicon/types/app/bsky/embed/record' +import { + isMain as isEmbedRecord, + isViewRecord, +} from '../../../lexicon/types/app/bsky/embed/record' import { isMain as isEmbedRecordWithMedia } from '../../../lexicon/types/app/bsky/embed/recordWithMedia' import { FeedViewPost } from '../../../lexicon/types/app/bsky/feed/defs' import { @@ -24,6 +27,8 @@ import { PostViews, PostEmbedViews, RecordEmbedViewRecordMap, + PostBlocksMap, + RecordEmbedViewRecord, FeedHydrationOptions, } from './types' import { LabelService, Labels } from '../label' @@ -265,13 +270,15 @@ export class FeedService { .as('requesterLike'), ]) .execute() - return posts.reduce( - (acc, cur) => ({ - ...acc, - [cur.uri]: cur, - }), - {} as PostInfoMap, - ) + return posts.reduce((acc, cur) => { + const { recordBytes, ...post } = cur + return Object.assign(acc, { + [post.uri]: { + ...post, + record: cborToLexRecord(recordBytes), + }, + }) + }, {} as PostInfoMap) } async getFeedGeneratorInfos( @@ -298,6 +305,7 @@ export class FeedService { actors?: ActorInfoMap posts?: PostInfoMap embeds?: PostEmbedViews + blocks?: PostBlocksMap labels?: Labels }, ): Promise { @@ -311,8 +319,10 @@ export class FeedService { precomputed?.labels ?? this.services.label.getLabelsForSubjects([...uris, ...dids]), ]) + const blocks = precomputed?.blocks ?? (await this.blocksForPosts(posts)) const embeds = - precomputed?.embeds ?? (await this.embedsForPosts(posts, requester)) + precomputed?.embeds ?? + (await this.embedsForPosts(posts, blocks, requester)) return uris.reduce((acc, cur) => { const view = this.views.formatPostView(cur, actors, posts, embeds, labels) @@ -354,13 +364,89 @@ export class FeedService { this.getPostInfos(Array.from(postUris), requester, options), this.services.label.getLabelsForSubjects([...postUris, ...actorDids]), ]) - const embeds = await this.embedsForPosts(posts, requester) + const blocks = await this.blocksForPosts(posts) + const embeds = await this.embedsForPosts(posts, blocks, requester) + + return this.views.formatFeed( + items, + actors, + posts, + embeds, + labels, + blocks, + options, + ) + } - return this.views.formatFeed(items, actors, posts, embeds, labels, options) + // applies blocks for visibility to third-parties (i.e. based on post content) + async blocksForPosts(posts: PostInfoMap): Promise { + const relationships: RelationshipPair[] = [] + const byPost: Record = {} + const didFromUri = (uri) => new AtUri(uri).host + for (const post of Object.values(posts)) { + // skip posts that we can't process or appear to already have been processed + if (!isPostRecord(post.record)) continue + if (byPost[post.uri]) continue + byPost[post.uri] = {} + // 3p block for replies + const parentUri = post.record.reply?.parent.uri + const parentDid = parentUri ? didFromUri(parentUri) : null + // 3p block for record embeds + const embedUris = nestedRecordUris([post.record]) + // gather actor relationships among posts + if (parentDid) { + const pair: RelationshipPair = [post.creator, parentDid] + relationships.push(pair) + byPost[post.uri].reply = pair + } + for (const embedUri of embedUris) { + const pair: RelationshipPair = [post.creator, didFromUri(embedUri)] + relationships.push(pair) + byPost[post.uri].embed = pair + } + } + // compute block state from all actor relationships among posts + const blockSet = await this.getBlockSet(relationships) + if (blockSet.empty()) return {} + const result: PostBlocksMap = {} + Object.entries(byPost).forEach(([uri, block]) => { + if (block.embed && blockSet.has(block.embed)) { + result[uri] ??= {} + result[uri].embed = true + } + if (block.reply && blockSet.has(block.reply)) { + result[uri] ??= {} + result[uri].reply = true + } + }) + return result + } + + private async getBlockSet(relationships: RelationshipPair[]) { + const { ref } = this.db.db.dynamic + const blockSet = new RelationshipSet() + if (!relationships.length) return blockSet + const relationshipSet = new RelationshipSet() + relationships.forEach((pair) => relationshipSet.add(pair)) + // compute actual block set from all actor relationships + const blockRows = await this.db.db + .selectFrom('actor_block') + .select(['creator', 'subjectDid']) // index-only columns + .where( + sql`(${ref('creator')}, ${ref('subjectDid')})`, + 'in', + valuesList( + relationshipSet.listAllPairs().map(([a, b]) => sql`${a}, ${b}`), + ), + ) + .execute() + blockRows.forEach((r) => blockSet.add([r.creator, r.subjectDid])) + return blockSet } async embedsForPosts( postInfos: PostInfoMap, + blocks: PostBlocksMap, requester: string | null, depth = 0, ) { @@ -383,14 +469,18 @@ export class FeedService { if (!recordEmbedViews[post.embed.record.uri]) continue postEmbedViews[uri] = { $type: 'app.bsky.embed.record#view', - record: recordEmbedViews[post.embed.record.uri], + record: applyEmbedBlock( + uri, + blocks, + recordEmbedViews[post.embed.record.uri], + ), } } else if (isEmbedRecordWithMedia(post.embed)) { const embedRecordView = recordEmbedViews[post.embed.record.record.uri] if (!embedRecordView) continue const formatted = this.views.getRecordWithMediaEmbedView( post.embed, - embedRecordView, + applyEmbedBlock(uri, blocks, embedRecordView), ) if (formatted) { postEmbedViews[uri] = formatted @@ -434,8 +524,10 @@ export class FeedService { this.getFeedGeneratorInfos(nestedFeedGenUris, requester), this.services.graph.getListViews(nestedListUris, requester), ]) + const deepBlocks = await this.blocksForPosts(postInfos) const deepEmbedViews = await this.embedsForPosts( postInfos, + deepBlocks, requester, depth + 1, ) @@ -497,9 +589,8 @@ const postRecordsFromInfos = ( ): { [uri: string]: PostRecord } => { const records: { [uri: string]: PostRecord } = {} for (const [uri, info] of Object.entries(infos)) { - const record = cborToLexRecord(info.recordBytes) - if (isPostRecord(record)) { - records[uri] = record + if (isPostRecord(info.record)) { + records[uri] = info.record } } return records @@ -519,3 +610,48 @@ const nestedRecordUris = (posts: PostRecord[]): string[] => { } return uris } + +type PostRelationships = { reply?: RelationshipPair; embed?: RelationshipPair } + +type RelationshipPair = [didA: string, didB: string] + +class RelationshipSet { + index = new Map>() + add([didA, didB]: RelationshipPair) { + const didAIdx = this.index.get(didA) ?? new Set() + const didBIdx = this.index.get(didB) ?? new Set() + if (!this.index.has(didA)) this.index.set(didA, didAIdx) + if (!this.index.has(didB)) this.index.set(didB, didBIdx) + didAIdx.add(didB) + didBIdx.add(didA) + } + has([didA, didB]: RelationshipPair) { + return !!this.index.get(didA)?.has(didB) + } + listAllPairs() { + const pairs: RelationshipPair[] = [] + for (const [didA, didBIdx] of this.index.entries()) { + for (const didB of didBIdx) { + pairs.push([didA, didB]) + } + } + return pairs + } + empty() { + return this.index.size === 0 + } +} + +function applyEmbedBlock( + uri: string, + blocks: PostBlocksMap, + view: RecordEmbedViewRecord, +): RecordEmbedViewRecord { + if (isViewRecord(view) && blocks[uri]?.embed) { + return { + $type: 'app.bsky.embed.record#viewBlocked', + uri: view.uri, + } + } + return view +} diff --git a/packages/pds/src/app-view/services/feed/types.ts b/packages/pds/src/app-view/services/feed/types.ts index 32076ddb06c..c0eaea16eef 100644 --- a/packages/pds/src/app-view/services/feed/types.ts +++ b/packages/pds/src/app-view/services/feed/types.ts @@ -1,3 +1,4 @@ +import { RepoRecord } from '@atproto/lexicon' import { View as ImagesEmbedView } from '../../../lexicon/types/app/bsky/embed/images' import { View as ExternalEmbedView } from '../../../lexicon/types/app/bsky/embed/external' import { @@ -33,7 +34,7 @@ export type PostInfo = { uri: string cid: string creator: string - recordBytes: Uint8Array + record: RepoRecord indexedAt: string likeCount: number | null repostCount: number | null @@ -45,6 +46,10 @@ export type PostInfo = { export type PostInfoMap = { [uri: string]: PostInfo } +export type PostBlocksMap = { + [uri: string]: { reply?: boolean; embed?: boolean } +} + export type ActorInfo = { did: string handle: string diff --git a/packages/pds/src/app-view/services/feed/views.ts b/packages/pds/src/app-view/services/feed/views.ts index b145269f010..27d25675598 100644 --- a/packages/pds/src/app-view/services/feed/views.ts +++ b/packages/pds/src/app-view/services/feed/views.ts @@ -1,4 +1,3 @@ -import { cborToLexRecord } from '@atproto/repo' import Database from '../../../db' import { FeedViewPost, @@ -29,6 +28,7 @@ import { MaybePostView, PostInfoMap, RecordEmbedViewRecord, + PostBlocksMap, FeedHydrationOptions, } from './types' import { Labels } from '../label' @@ -82,6 +82,7 @@ export class FeedViews { posts: PostInfoMap, embeds: PostEmbedViews, labels: Labels, + blocks: PostBlocksMap, opts?: FeedHydrationOptions, ): FeedViewPost[] { const feed: FeedViewPost[] = [] @@ -95,7 +96,7 @@ export class FeedViews { opts, ) // skip over not found & blocked posts - if (!post) { + if (!post || blocks[post.uri]?.reply) { continue } const feedPost = { post } @@ -122,6 +123,7 @@ export class FeedViews { posts, embeds, labels, + blocks, opts, ) const replyRoot = this.formatMaybePostView( @@ -130,6 +132,7 @@ export class FeedViews { posts, embeds, labels, + blocks, opts, ) if (replyRoot && replyParent) { @@ -165,7 +168,7 @@ export class FeedViews { takedownId: opts?.includeSoftDeleted ? post.takedownId ?? null : undefined, - record: cborToLexRecord(post.recordBytes), + record: post.record, embed: embeds[uri], replyCount: post.replyCount ?? 0, repostCount: post.repostCount ?? 0, @@ -185,6 +188,7 @@ export class FeedViews { posts: PostInfoMap, embeds: PostEmbedViews, labels: Labels, + blocks: PostBlocksMap, opts?: FeedHydrationOptions, ): MaybePostView | undefined { const post = this.formatPostView(uri, actors, posts, embeds, labels, opts) @@ -192,7 +196,11 @@ export class FeedViews { if (!opts?.usePostViewUnion) return return this.notFoundPost(uri) } - if (post.author.viewer?.blockedBy || post.author.viewer?.blocking) { + if ( + post.author.viewer?.blockedBy || + post.author.viewer?.blocking || + blocks[uri]?.reply + ) { if (!opts?.usePostViewUnion) return return this.blockedPost(uri) } diff --git a/packages/pds/tests/views/blocks.test.ts b/packages/pds/tests/views/blocks.test.ts index a3815a6a668..56afc2a94d4 100644 --- a/packages/pds/tests/views/blocks.test.ts +++ b/packages/pds/tests/views/blocks.test.ts @@ -1,17 +1,25 @@ -import AtpAgent from '@atproto/api' -import { runTestServer, CloseFn, TestServerInfo, forSnapshot } from '../_util' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' +import assert from 'assert' +import AtpAgent, { AtUri } from '@atproto/api' import { RecordRef } from '@atproto/bsky/tests/seeds/client' import { BlockedActorError } from '@atproto/api/src/client/types/app/bsky/feed/getAuthorFeed' import { BlockedByActorError } from '@atproto/api/src/client/types/app/bsky/feed/getAuthorFeed' +import { isThreadViewPost } from '@atproto/api/src/client/types/app/bsky/feed/defs' +import { + isViewRecord as isEmbedViewRecord, + isViewBlocked as isEmbedViewBlocked, +} from '@atproto/api/src/client/types/app/bsky/embed/record' +import { runTestServer, CloseFn, TestServerInfo, forSnapshot } from '../_util' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' describe('pds views with blocking', () => { let server: TestServerInfo let agent: AtpAgent let close: CloseFn let sc: SeedClient + let danBlockCarol: { uri: string } let aliceReplyToDan: { ref: RecordRef } + let carolReplyToDan: { ref: RecordRef } let alice: string let carol: string @@ -31,18 +39,24 @@ describe('pds views with blocking', () => { // add follows to ensure blocks work even w follows await sc.follow(carol, dan) await sc.follow(dan, carol) - // dan blocks carol - await agent.api.app.bsky.graph.block.create( - { repo: dan }, - { createdAt: new Date().toISOString(), subject: carol }, - sc.getHeaders(dan), - ) aliceReplyToDan = await sc.reply( alice, sc.posts[dan][0].ref, sc.posts[dan][0].ref, 'alice replies to dan', ) + carolReplyToDan = await sc.reply( + carol, + sc.posts[dan][0].ref, + sc.posts[dan][0].ref, + 'carol replies to dan', + ) + // dan blocks carol + danBlockCarol = await agent.api.app.bsky.graph.block.create( + { repo: dan }, + { createdAt: new Date().toISOString(), subject: carol }, + sc.getHeaders(dan), + ) await server.processAll() }) @@ -51,7 +65,6 @@ describe('pds views with blocking', () => { }) it('blocks thread post', async () => { - const { carol, dan } = sc.dids const { data: threadAlice } = await agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: sc.posts[carol][0].ref.uriStr }, { headers: sc.getHeaders(dan) }, @@ -293,6 +306,132 @@ describe('pds views with blocking', () => { expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy() }) + it('does not serve blocked replies', async () => { + const getThreadPostUri = (r) => r?.['post']?.['uri'] + // reply then block + const { data: replyThenBlock } = + await agent.api.app.bsky.feed.getPostThread( + { depth: 1, uri: sc.posts[dan][0].ref.uriStr }, + { headers: sc.getHeaders(alice) }, + ) + assert(isThreadViewPost(replyThenBlock.thread)) + expect(replyThenBlock.thread.replies?.map(getThreadPostUri)).toEqual([ + aliceReplyToDan.ref.uriStr, + ]) + + // unblock + await agent.api.app.bsky.graph.block.delete( + { repo: dan, rkey: new AtUri(danBlockCarol.uri).rkey }, + sc.getHeaders(dan), + ) + const { data: unblock } = await agent.api.app.bsky.feed.getPostThread( + { depth: 1, uri: sc.posts[dan][0].ref.uriStr }, + { headers: sc.getHeaders(alice) }, + ) + assert(isThreadViewPost(unblock.thread)) + expect(unblock.thread.replies?.map(getThreadPostUri)).toEqual([ + carolReplyToDan.ref.uriStr, + aliceReplyToDan.ref.uriStr, + ]) + + // block then reply + danBlockCarol = await agent.api.app.bsky.graph.block.create( + { repo: dan }, + { createdAt: new Date().toISOString(), subject: carol }, + sc.getHeaders(dan), + ) + const carolReplyToDan2 = await sc.reply( + carol, + sc.posts[dan][1].ref, + sc.posts[dan][1].ref, + 'carol replies to dan again', + ) + const { data: blockThenReply } = + await agent.api.app.bsky.feed.getPostThread( + { depth: 1, uri: sc.posts[dan][0].ref.uriStr }, + { headers: sc.getHeaders(alice) }, + ) + assert(isThreadViewPost(blockThenReply.thread)) + expect(replyThenBlock.thread.replies?.map(getThreadPostUri)).toEqual([ + aliceReplyToDan.ref.uriStr, + ]) + + // cleanup + await agent.api.app.bsky.feed.post.delete( + { repo: carol, rkey: carolReplyToDan2.ref.uri.rkey }, + sc.getHeaders(carol), + ) + }) + + it('does not serve blocked embeds to third-party', async () => { + // embed then block + const { data: embedThenBlock } = + await agent.api.app.bsky.feed.getPostThread( + { depth: 0, uri: sc.posts[dan][1].ref.uriStr }, + { headers: sc.getHeaders(alice) }, + ) + assert(isThreadViewPost(embedThenBlock.thread)) + assert(isEmbedViewBlocked(embedThenBlock.thread.post.embed?.record)) + + // unblock + await agent.api.app.bsky.graph.block.delete( + { repo: dan, rkey: new AtUri(danBlockCarol.uri).rkey }, + sc.getHeaders(dan), + ) + const { data: unblock } = await agent.api.app.bsky.feed.getPostThread( + { depth: 0, uri: sc.posts[dan][1].ref.uriStr }, + { headers: sc.getHeaders(alice) }, + ) + assert(isThreadViewPost(unblock.thread)) + assert(isEmbedViewRecord(unblock.thread.post.embed?.record)) + + // block then embed + danBlockCarol = await agent.api.app.bsky.graph.block.create( + { repo: dan }, + { createdAt: new Date().toISOString(), subject: carol }, + sc.getHeaders(dan), + ) + const carolEmbedsDan = await sc.post( + carol, + 'carol embeds dan', + undefined, + undefined, + sc.posts[dan][0].ref, + ) + const { data: blockThenEmbed } = + await agent.api.app.bsky.feed.getPostThread( + { depth: 0, uri: carolEmbedsDan.ref.uriStr }, + { headers: sc.getHeaders(alice) }, + ) + assert(isThreadViewPost(blockThenEmbed.thread)) + assert(isEmbedViewBlocked(blockThenEmbed.thread.post.embed?.record)) + + // cleanup + await agent.api.app.bsky.feed.post.delete( + { repo: carol, rkey: carolEmbedsDan.ref.uri.rkey }, + sc.getHeaders(carol), + ) + }) + + it('applies third-party blocking rules in feeds.', async () => { + // alice follows carol and dan, block exists between carol and dan. + const replyBlockedUri = carolReplyToDan.ref.uriStr + const embedBlockedUri = sc.posts[dan][1].ref.uriStr + const { data: timeline } = await agent.api.app.bsky.feed.getTimeline( + { limit: 100 }, + { headers: sc.getHeaders(alice) }, + ) + const replyBlockedPost = timeline.feed.find( + (item) => item.post.uri === replyBlockedUri, + ) + expect(replyBlockedPost).toBeUndefined() + const embedBlockedPost = timeline.feed.find( + (item) => item.post.uri === embedBlockedUri, + ) + assert(embedBlockedPost) + assert(isEmbedViewBlocked(embedBlockedPost.post.embed?.record)) + }) + it('returns a list of blocks', async () => { await agent.api.app.bsky.graph.block.create( { repo: dan }, From d9f73769e75f492cf71b6c956cafcd0d1a38df48 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Mon, 24 Jul 2023 20:53:06 -0400 Subject: [PATCH 062/237] Batch outgoing sequencing (#1298) * batch outgoing sequencing for postgres * small reorg of sequence consumer to repoll w/o pg notify saturation * patch up a couple sqlite tests * use a postgres sequence for repo-seq seqs * increase sequence gap * build branch w no migration * Sequencer benching (#1359) * benchmark sequencer * perf improvements & refactor * bugfix * no optimistic backfill pagination * tidy * ready for merge * add back in logging * missing async * tidy --------- Co-authored-by: dholms --- packages/pds/bench/sequencer.bench.ts | 138 ++++++++++++++++++ packages/pds/jest.bench.config.js | 8 + packages/pds/package.json | 1 + ...18T170914772Z-sequencer-leader-sequence.ts | 25 ++++ packages/pds/src/db/migrations/index.ts | 1 + packages/pds/src/db/tables/repo-seq.ts | 2 + packages/pds/src/index.ts | 12 +- packages/pds/src/sequencer/outbox.ts | 7 +- .../pds/src/sequencer/sequencer-leader.ts | 86 ++++++----- packages/pds/src/sequencer/sequencer.ts | 34 ++--- 10 files changed, 245 insertions(+), 69 deletions(-) create mode 100644 packages/pds/bench/sequencer.bench.ts create mode 100644 packages/pds/jest.bench.config.js create mode 100644 packages/pds/src/db/migrations/20230718T170914772Z-sequencer-leader-sequence.ts diff --git a/packages/pds/bench/sequencer.bench.ts b/packages/pds/bench/sequencer.bench.ts new file mode 100644 index 00000000000..00c3e2c21c4 --- /dev/null +++ b/packages/pds/bench/sequencer.bench.ts @@ -0,0 +1,138 @@ +import { randomBytes } from '@atproto/crypto' +import { cborEncode } from '@atproto/common' +import { TestServerInfo, runTestServer } from '../tests/_util' +import { randomCid } from '@atproto/repo/tests/_util' +import { BlockMap, blocksToCarFile } from '@atproto/repo' +import { byFrame } from '@atproto/xrpc-server' +import { WebSocket } from 'ws' +import { Database } from '../src' + +describe('sequencer bench', () => { + let server: TestServerInfo + + let db: Database + + beforeAll(async () => { + server = await runTestServer({ + dbPostgresSchema: 'sequencer_bench', + maxSubscriptionBuffer: 20000, + }) + if (!server.ctx.cfg.dbPostgresUrl) { + throw new Error('no postgres url') + } + db = Database.postgres({ + url: server.ctx.cfg.dbPostgresUrl, + schema: server.ctx.cfg.dbPostgresSchema, + txLockNonce: server.ctx.cfg.dbTxLockNonce, + poolSize: 50, + }) + + server.ctx.sequencerLeader?.destroy() + }) + + afterAll(async () => { + await server.close() + }) + + const doWrites = async (batches: number, batchSize: number) => { + const cid = await randomCid() + const blocks = new BlockMap() + await blocks.add(randomBytes(500)) + await blocks.add(randomBytes(500)) + await blocks.add(randomBytes(500)) + await blocks.add(randomBytes(500)) + await blocks.add(randomBytes(500)) + await blocks.add(randomBytes(500)) + + const car = await blocksToCarFile(cid, blocks) + const evt = { + rebase: false, + tooBig: false, + repo: 'did:plc:123451234', + commit: cid, + prev: cid, + ops: [{ action: 'create', path: 'app.bsky.feed.post/abcdefg1234', cid }], + blocks: car, + blobs: [], + } + const encodeEvt = cborEncode(evt) + + const promises: Promise[] = [] + for (let i = 0; i < batches; i++) { + const rows: any[] = [] + for (let j = 0; j < batchSize; j++) { + rows.push({ + did: 'did:web:example.com', + eventType: 'append', + event: encodeEvt, + sequencedAt: new Date().toISOString(), + }) + } + const insert = db.db.insertInto('repo_seq').values(rows).execute() + promises.push(insert) + } + await Promise.all(promises) + } + + const readAll = async ( + totalToRead: number, + cursor?: number, + ): Promise => { + const serverHost = server.url.replace('http://', '') + let url = `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos` + if (cursor !== undefined) { + url += `?cursor=${cursor}` + } + const ws = new WebSocket(url) + + let start = Date.now() + let count = 0 + const gen = byFrame(ws) + for await (const _frame of gen) { + if (count === 0) { + start = Date.now() + } + count++ + if (count >= totalToRead) { + break + } + } + if (count < totalToRead) { + throw new Error('Did not read full websocket') + } + return Date.now() - start + } + + it('benches', async () => { + const BATCHES = 100 + const BATCH_SIZE = 100 + const TOTAL = BATCHES * BATCH_SIZE + const readAllPromise = readAll(TOTAL, 0) + + const start = Date.now() + + await doWrites(BATCHES, BATCH_SIZE) + const setup = Date.now() + + await server.ctx.sequencerLeader?.sequenceOutgoing() + const sequencingTime = Date.now() - setup + + const liveTailTime = await readAllPromise + const backfillTime = await readAll(TOTAL, 0) + + console.log(` +${TOTAL} events +Setup: ${setup - start} ms +Sequencing: ${sequencingTime} ms +Sequencing Rate: ${formatRate(TOTAL, sequencingTime)} evt/s +Live tail: ${liveTailTime} ms +Live tail Rate: ${formatRate(TOTAL, liveTailTime)} evt/s +Backfilled: ${backfillTime} ms +Backfill Rate: ${formatRate(TOTAL, backfillTime)} evt/s`) + }) +}) + +const formatRate = (evts: number, timeMs: number): string => { + const evtPerSec = (evts * 1000) / timeMs + return evtPerSec.toFixed(3) +} diff --git a/packages/pds/jest.bench.config.js b/packages/pds/jest.bench.config.js new file mode 100644 index 00000000000..3207cdd8b97 --- /dev/null +++ b/packages/pds/jest.bench.config.js @@ -0,0 +1,8 @@ +const base = require('./jest.config') + +module.exports = { + ...base, + roots: ['/bench'], + testRegex: '(.*.bench)', + testTimeout: 3000000, +} diff --git a/packages/pds/package.json b/packages/pds/package.json index 5c5f46ac407..89781642f74 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -15,6 +15,7 @@ "postbuild": "tsc --build tsconfig.build.json", "start": "node dist/bin.js", "test": "../pg/with-test-db.sh jest", + "bench": "../pg/with-test-db.sh jest --config jest.bench.config.js", "test:sqlite": "jest --testPathIgnorePatterns /tests/proxied/*", "test:log": "tail -50 test.log | pino-pretty", "test:updateSnapshot": "jest --updateSnapshot", diff --git a/packages/pds/src/db/migrations/20230718T170914772Z-sequencer-leader-sequence.ts b/packages/pds/src/db/migrations/20230718T170914772Z-sequencer-leader-sequence.ts new file mode 100644 index 00000000000..aae6db339b9 --- /dev/null +++ b/packages/pds/src/db/migrations/20230718T170914772Z-sequencer-leader-sequence.ts @@ -0,0 +1,25 @@ +import { Kysely, sql } from 'kysely' +import { Dialect } from '..' + +export async function up(db: Kysely, dialect: Dialect): Promise { + if (dialect === 'sqlite') return + const res = await db + .selectFrom('repo_seq') + .select('seq') + .where('seq', 'is not', null) + .orderBy('seq', 'desc') + .limit(1) + .executeTakeFirst() + const startAt = res?.seq ? res.seq + 50000 : 1 + await sql`CREATE SEQUENCE repo_seq_sequence START ${sql.literal( + startAt, + )};`.execute(db) +} + +export async function down( + db: Kysely, + dialect: Dialect, +): Promise { + if (dialect === 'sqlite') return + await sql`DROP SEQUENCE repo_seq_sequence;`.execute(db) +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index 05bbb8c6fcc..11b2432b9c6 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -54,3 +54,4 @@ export * as _20230529T222706121Z from './20230529T222706121Z-suggested-follows' export * as _20230530T213530067Z from './20230530T213530067Z-rebase-indices' export * as _20230605T235529700Z from './20230605T235529700Z-outgoing-repo-seq' export * as _20230703T044601833Z from './20230703T044601833Z-feed-and-label-indices' +export * as _20230718T170914772Z from './20230718T170914772Z-sequencer-leader-sequence' diff --git a/packages/pds/src/db/tables/repo-seq.ts b/packages/pds/src/db/tables/repo-seq.ts index 7d1034048bc..ffd482c327a 100644 --- a/packages/pds/src/db/tables/repo-seq.ts +++ b/packages/pds/src/db/tables/repo-seq.ts @@ -2,6 +2,8 @@ import { Generated, GeneratedAlways, Insertable, Selectable } from 'kysely' export type EventType = 'append' | 'rebase' | 'handle' | 'migrate' | 'tombstone' +export const REPO_SEQ_SEQUENCE = 'repo_seq_sequence' + export interface RepoSeq { id: GeneratedAlways seq: number | null diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index e2c4187ae6a..f1e0770e5b4 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -284,12 +284,14 @@ export class PDS { ) }, 10000) } - this.sequencerStatsInterval = setInterval(() => { + this.sequencerStatsInterval = setInterval(async () => { if (this.ctx.sequencerLeader?.isLeader) { - seqLogger.info( - { seq: this.ctx.sequencerLeader.peekSeqVal() }, - 'sequencer leader stats', - ) + try { + const seq = await this.ctx.sequencerLeader.lastSeq() + seqLogger.info({ seq }, 'sequencer leader stats') + } catch (err) { + seqLogger.error({ err }, 'error getting last seq') + } } }, 500) appviewConsumers.listen(this.ctx) diff --git a/packages/pds/src/sequencer/outbox.ts b/packages/pds/src/sequencer/outbox.ts index f0f62421a3d..d56e6bd0fb6 100644 --- a/packages/pds/src/sequencer/outbox.ts +++ b/packages/pds/src/sequencer/outbox.ts @@ -104,18 +104,19 @@ export class Outbox { // yields only historical events async *getBackfill(backfillCursor: number, backfillTime?: string) { + const PAGE_SIZE = 200 while (true) { const evts = await this.sequencer.requestSeqRange({ earliestTime: backfillTime, earliestSeq: this.lastSeen > -1 ? this.lastSeen : backfillCursor, - limit: 100, + limit: PAGE_SIZE, }) for (const evt of evts) { yield evt } - // if we're within 50 of the sequencer, we call it good & switch to cutover + // if we're within half a pagesize of the sequencer, we call it good & switch to cutover const seqCursor = this.sequencer.lastSeen ?? -1 - if (seqCursor - this.lastSeen < 100) break + if (seqCursor - this.lastSeen < PAGE_SIZE / 2) break if (evts.length < 1) break } } diff --git a/packages/pds/src/sequencer/sequencer-leader.ts b/packages/pds/src/sequencer/sequencer-leader.ts index fab3a3b4602..34afbacea13 100644 --- a/packages/pds/src/sequencer/sequencer-leader.ts +++ b/packages/pds/src/sequencer/sequencer-leader.ts @@ -1,8 +1,11 @@ +import { sql } from 'kysely' import { DisconnectError } from '@atproto/xrpc-server' -import { chunkArray, jitter, wait } from '@atproto/common' +import { jitter, wait } from '@atproto/common' import { Leader } from '../db/leader' import { seqLogger as log } from '../logger' import Database from '../db' +import { REPO_SEQ_SEQUENCE } from '../db/tables/repo-seq' +import { countAll } from '../db/util' export const SEQUENCER_LEADER_ID = 1100 @@ -12,21 +15,11 @@ export class SequencerLeader { destroyed = false polling = false queued = false - private lastSeq: number constructor(public db: Database, lockId = SEQUENCER_LEADER_ID) { this.leader = new Leader(lockId, this.db) } - nextSeqVal(): number { - this.lastSeq++ - return this.lastSeq - } - - peekSeqVal(): number | undefined { - return this.lastSeq - } - get isLeader() { return !!this.leader.session } @@ -37,15 +30,6 @@ export class SequencerLeader { while (!this.destroyed) { try { const { ran } = await this.leader.run(async ({ signal }) => { - const res = await this.db.db - .selectFrom('repo_seq') - .select('seq') - .where('seq', 'is not', null) - .orderBy('seq', 'desc') - .limit(1) - .executeTakeFirst() - this.lastSeq = res?.seq ?? 0 - const seqListener = () => { if (this.polling) { this.queued = true @@ -111,38 +95,52 @@ export class SequencerLeader { } async sequenceOutgoing() { - const unsequenced = await this.getUnsequenced() - const chunks = chunkArray(unsequenced, 2000) - for (const chunk of chunks) { - await this.db.transaction(async (dbTxn) => { - await Promise.all( - chunk.map(async (row) => { - await dbTxn.db - .updateTable('repo_seq') - .set({ seq: this.nextSeqVal() }) - .where('id', '=', row.id) - .execute() - await this.db.notify('outgoing_repo_seq') - }), - ) - }) - await this.db.notify('outgoing_repo_seq') - } + await this.db.db + .updateTable('repo_seq') + .from((qb) => + qb + .selectFrom('repo_seq') + .select([ + 'id as update_id', + sql`nextval(${sql.literal(REPO_SEQ_SEQUENCE)})`.as( + 'update_seq', + ), + ]) + .where('seq', 'is', null) + .orderBy('id', 'asc') + .as('update'), + ) + .set({ seq: sql`update_seq::bigint` }) + .whereRef('id', '=', 'update_id') + .execute() + + await this.db.notify('outgoing_repo_seq') } - async getUnsequenced() { - return this.db.db + async getUnsequencedCount() { + const res = await this.db.db .selectFrom('repo_seq') .where('seq', 'is', null) - .select('id') - .orderBy('id', 'asc') - .execute() + .select(countAll.as('count')) + .executeTakeFirst() + return res?.count ?? 0 } async isCaughtUp(): Promise { if (this.db.dialect === 'sqlite') return true - const unsequenced = await this.getUnsequenced() - return unsequenced.length === 0 + const count = await this.getUnsequencedCount() + return count === 0 + } + + async lastSeq(): Promise { + const res = await this.db.db + .selectFrom('repo_seq') + .select('seq') + .where('seq', 'is not', null) + .orderBy('seq', 'desc') + .limit(1) + .executeTakeFirst() + return res?.seq ?? 0 } destroy() { diff --git a/packages/pds/src/sequencer/sequencer.ts b/packages/pds/src/sequencer/sequencer.ts index 61bcfa0efa7..7c678bcc711 100644 --- a/packages/pds/src/sequencer/sequencer.ts +++ b/packages/pds/src/sequencer/sequencer.ts @@ -3,8 +3,8 @@ import TypedEmitter from 'typed-emitter' import Database from '../db' import { seqLogger as log } from '../logger' import { RepoSeqEntry } from '../db/tables/repo-seq' -import { cborDecode, check } from '@atproto/common' -import { commitEvt, handleEvt, SeqEvt, tombstoneEvt } from './events' +import { cborDecode } from '@atproto/common' +import { CommitEvt, HandleEvt, SeqEvt, TombstoneEvt } from './events' export * from './events' @@ -24,11 +24,10 @@ export class Sequencer extends (EventEmitter as new () => SequencerEmitter) { this.lastSeen = curr.seq ?? 0 } this.db.channels.outgoing_repo_seq.addListener('message', () => { - if (this.polling) { - this.queued = true - } else { - this.polling = true + if (!this.polling) { this.pollDb() + } else { + this.queued = true // poll again once current poll completes } }) } @@ -95,26 +94,26 @@ export class Sequencer extends (EventEmitter as new () => SequencerEmitter) { continue } const evt = cborDecode(row.event) - if (check.is(evt, commitEvt)) { + if (row.eventType === 'append' || row.eventType === 'rebase') { seqEvts.push({ type: 'commit', seq: row.seq, time: row.sequencedAt, - evt, + evt: evt as CommitEvt, }) - } else if (check.is(evt, handleEvt)) { + } else if (row.eventType === 'handle') { seqEvts.push({ type: 'handle', seq: row.seq, time: row.sequencedAt, - evt, + evt: evt as HandleEvt, }) - } else if (check.is(evt, tombstoneEvt)) { + } else if (row.eventType === 'tombstone') { seqEvts.push({ type: 'tombstone', seq: row.seq, time: row.sequencedAt, - evt, + evt: evt as TombstoneEvt, }) } } @@ -124,21 +123,22 @@ export class Sequencer extends (EventEmitter as new () => SequencerEmitter) { async pollDb() { try { + this.polling = true const evts = await this.requestSeqRange({ earliestSeq: this.lastSeen, - limit: 50, + limit: 1000, }) if (evts.length > 0) { + this.queued = true // should poll again immediately this.emit('events', evts) this.lastSeen = evts.at(-1)?.seq ?? this.lastSeen } } catch (err) { log.error({ err, lastSeen: this.lastSeen }, 'sequencer failed to poll db') } finally { - // check if we should continue polling - if (this.queued === false) { - this.polling = false - } else { + this.polling = false + if (this.queued) { + // if queued, poll again this.queued = false this.pollDb() } From 46beab62ee1cee1505dd42030ea1033149960005 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 25 Jul 2023 18:54:18 -0500 Subject: [PATCH 063/237] Fix flaky partition queue test (#1386) fix flaky partition queue test --- packages/bsky/tests/subscription/util.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bsky/tests/subscription/util.test.ts b/packages/bsky/tests/subscription/util.test.ts index 20448b17ef0..497532f643b 100644 --- a/packages/bsky/tests/subscription/util.test.ts +++ b/packages/bsky/tests/subscription/util.test.ts @@ -95,11 +95,11 @@ describe('subscription utils', () => { const complete: number[] = [] // partition 1 items start slow but get faster: slow should still complete first. partitioned.add('1', async () => { - await wait(10) + await wait(30) complete.push(11) }) partitioned.add('1', async () => { - await wait(5) + await wait(20) complete.push(12) }) partitioned.add('1', async () => { @@ -121,7 +121,7 @@ describe('subscription utils', () => { complete.push(23) }) partitioned.add('2', async () => { - await wait(15) + await wait(60) complete.push(24) }) expect(partitioned.partitions.size).toEqual(2) From a2655a40014d78cb1d95ca582bd97023badc6fbb Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 25 Jul 2023 22:41:10 -0500 Subject: [PATCH 064/237] Move rkey check to prepareCreate (#1385) * move rkey check to prepareCreate * allow updates on applyWrites * gen rkey in prepareCreate * fix up tests --- .../src/api/com/atproto/repo/applyWrites.ts | 15 +- .../src/api/com/atproto/repo/createRecord.ts | 9 +- .../pds/src/api/com/atproto/repo/putRecord.ts | 15 - packages/pds/src/repo/prepare.ts | 24 + .../tests/__snapshots__/indexing.test.ts.snap | 60 - packages/pds/tests/crud.test.ts | 28 +- .../__snapshots__/sync.test.ts.snap | 1224 ----------------- packages/pds/tests/event-stream/sync.test.ts | 151 -- packages/pds/tests/indexing.test.ts | 35 - packages/pds/tests/races.test.ts | 3 +- 10 files changed, 49 insertions(+), 1515 deletions(-) delete mode 100644 packages/pds/tests/event-stream/__snapshots__/sync.test.ts.snap delete mode 100644 packages/pds/tests/event-stream/sync.test.ts diff --git a/packages/pds/src/api/com/atproto/repo/applyWrites.ts b/packages/pds/src/api/com/atproto/repo/applyWrites.ts index 1cee83bdbce..5dc65100855 100644 --- a/packages/pds/src/api/com/atproto/repo/applyWrites.ts +++ b/packages/pds/src/api/com/atproto/repo/applyWrites.ts @@ -1,6 +1,6 @@ import { CID } from 'multiformats/cid' import { InvalidRequestError, AuthRequiredError } from '@atproto/xrpc-server' -import { prepareCreate, prepareDelete } from '../../../../repo' +import { prepareCreate, prepareDelete, prepareUpdate } from '../../../../repo' import { Server } from '../../../../lexicon' import { isCreate, @@ -38,11 +38,6 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError('Too many writes. Max: 200') } - const hasUpdate = tx.writes.some(isUpdate) - if (hasUpdate) { - throw new InvalidRequestError(`Updates are not yet supported.`) - } - let writes: PreparedWrite[] try { writes = await Promise.all( @@ -55,6 +50,14 @@ export default function (server: Server, ctx: AppContext) { rkey: write.rkey, validate, }) + } else if (isUpdate(write)) { + return prepareUpdate({ + did, + collection: write.collection, + record: write.value, + rkey: write.rkey, + validate, + }) } else if (isDelete(write)) { return prepareDelete({ did, diff --git a/packages/pds/src/api/com/atproto/repo/createRecord.ts b/packages/pds/src/api/com/atproto/repo/createRecord.ts index 0f28f8cbb4f..08ff46ecf78 100644 --- a/packages/pds/src/api/com/atproto/repo/createRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/createRecord.ts @@ -1,5 +1,4 @@ import { CID } from 'multiformats/cid' -import { TID } from '@atproto/common' import { InvalidRequestError, AuthRequiredError } from '@atproto/xrpc-server' import { prepareCreate } from '../../../../repo' import { Server } from '../../../../lexicon' @@ -33,12 +32,6 @@ export default function (server: Server, ctx: AppContext) { 'Unvalidated writes are not yet supported.', ) } - if (collection === ids.AppBskyFeedPost && rkey) { - throw new InvalidRequestError( - 'Custom rkeys for post records are not currently supported.', - ) - } - const swapCommitCid = swapCommit ? CID.parse(swapCommit) : undefined let write: PreparedCreate @@ -47,7 +40,7 @@ export default function (server: Server, ctx: AppContext) { did, collection, record, - rkey: rkey || TID.nextStr(), + rkey, validate, }) } catch (err) { diff --git a/packages/pds/src/api/com/atproto/repo/putRecord.ts b/packages/pds/src/api/com/atproto/repo/putRecord.ts index dc06efbb071..b0ca8f9cd95 100644 --- a/packages/pds/src/api/com/atproto/repo/putRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/putRecord.ts @@ -2,7 +2,6 @@ import { CID } from 'multiformats/cid' import { AtUri } from '@atproto/uri' import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import { ids } from '../../../../lexicon/lexicons' import { prepareUpdate, prepareCreate } from '../../../../repo' import AppContext from '../../../../context' import { @@ -14,12 +13,6 @@ import { } from '../../../../repo' import { ConcurrentWriteError } from '../../../../services/repo' -const ALLOWED_PUTS = [ - ids.AppBskyActorProfile, - ids.AppBskyGraphList, - ids.AppBskyFeedGenerator, -] - export default function (server: Server, ctx: AppContext) { server.com.atproto.repo.putRecord({ auth: ctx.accessVerifierCheckTakedown, @@ -41,14 +34,6 @@ export default function (server: Server, ctx: AppContext) { if (did !== auth.credentials.did) { throw new AuthRequiredError() } - if (!ALLOWED_PUTS.includes(collection)) { - // @TODO temporary - throw new InvalidRequestError( - `Temporarily only accepting puts for collections: ${ALLOWED_PUTS.join( - ', ', - )}`, - ) - } if (validate === false) { throw new InvalidRequestError( 'Unvalidated writes are not yet supported.', diff --git a/packages/pds/src/repo/prepare.ts b/packages/pds/src/repo/prepare.ts index 7a3e9afea05..518844f76f7 100644 --- a/packages/pds/src/repo/prepare.ts +++ b/packages/pds/src/repo/prepare.ts @@ -34,6 +34,7 @@ import { import { isRecord as isList } from '../lexicon/types/app/bsky/graph/list' import { isRecord as isProfile } from '../lexicon/types/app/bsky/actor/profile' import { hasExplicitSlur } from '../content-reporter/explicit-slurs' +import { InvalidRequestError } from '@atproto/xrpc-server' // @TODO do this dynamically off of schemas export const blobsForWrite = (record: unknown): PreparedBlobRef[] => { @@ -154,6 +155,13 @@ export const prepareCreate = async (opts: { if (validate) { assertValidRecord(record) } + if (collection === lex.ids.AppBskyFeedPost && opts.rkey) { + // @TODO temporary + throw new InvalidRequestError( + 'Custom rkeys for post records are not currently supported.', + ) + } + const rkey = opts.rkey || TID.nextStr() assertNoExplicitSlurs(rkey, record) return { @@ -166,6 +174,13 @@ export const prepareCreate = async (opts: { } } +// only allow PUTs to certain collections +const ALLOWED_PUTS = [ + lex.ids.AppBskyActorProfile, + lex.ids.AppBskyGraphList, + lex.ids.AppBskyFeedGenerator, +] + export const prepareUpdate = async (opts: { did: string collection: string @@ -175,6 +190,15 @@ export const prepareUpdate = async (opts: { validate?: boolean }): Promise => { const { did, collection, rkey, swapCid, validate = true } = opts + if (!ALLOWED_PUTS.includes(collection)) { + // @TODO temporary + throw new InvalidRequestError( + `Temporarily only accepting updates for collections: ${ALLOWED_PUTS.join( + ', ', + )}`, + ) + } + const record = setCollectionName(collection, opts.record, validate) if (validate) { assertValidRecord(record) diff --git a/packages/pds/tests/__snapshots__/indexing.test.ts.snap b/packages/pds/tests/__snapshots__/indexing.test.ts.snap index 5e070726fed..b1810d28696 100644 --- a/packages/pds/tests/__snapshots__/indexing.test.ts.snap +++ b/packages/pds/tests/__snapshots__/indexing.test.ts.snap @@ -50,55 +50,6 @@ Object { `; exports[`indexing indexes posts. 2`] = ` -Object { - "thread": Object { - "$type": "app.bsky.feed.defs#threadViewPost", - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(1)", - }, - ], - "index": Object { - "byteEnd": 11, - "byteStart": 0, - }, - }, - ], - "text": "@carol.test how are you?", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(0)", - "viewer": Object {}, - }, - "replies": Array [], - }, -} -`; - -exports[`indexing indexes posts. 3`] = ` Object { "createNotifications": Array [ Object { @@ -112,17 +63,6 @@ Object { }, ], "deleteNotifications": Array [], - "updateNotifications": Array [ - Object { - "author": "user(1)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "reason": "mention", - "reasonSubject": null, - "recordCid": "cids(1)", - "recordUri": "record(0)", - "userDid": "user(2)", - }, - ], } `; diff --git a/packages/pds/tests/crud.test.ts b/packages/pds/tests/crud.test.ts index d4425d72586..bf437390a14 100644 --- a/packages/pds/tests/crud.test.ts +++ b/packages/pds/tests/crud.test.ts @@ -432,9 +432,9 @@ describe('crud operations', () => { }) }) - it('temporarily only puts profile records', async () => { + it('temporarily only allows updates to profile', async () => { const { repo } = bobAgent.api.com.atproto - const put = repo.putRecord({ + const put = await repo.putRecord({ repo: bob.did, collection: ids.AppBskyGraphFollow, rkey: TID.nextStr(), @@ -443,8 +443,18 @@ describe('crud operations', () => { createdAt: new Date().toISOString(), }, }) - await expect(put).rejects.toThrow( - 'Temporarily only accepting puts for collections: app.bsky.actor.profile, app.bsky.graph.list', + const edit = repo.putRecord({ + repo: bob.did, + collection: ids.AppBskyGraphFollow, + rkey: new AtUri(put.data.uri).rkey, + record: { + subject: bob.did, + createdAt: new Date().toISOString(), + }, + }) + + await expect(edit).rejects.toThrow( + 'Temporarily only accepting updates for collections: app.bsky.actor.profile, app.bsky.graph.list, app.bsky.feed.generator', ) }) @@ -781,7 +791,6 @@ describe('crud operations', () => { it('applyWrites succeeds on proper commit cas', async () => { const { repo, sync } = aliceAgent.api.com.atproto const { data: head } = await sync.getHead({ did: alice.did }) - const rkey = TID.nextStr() await repo.applyWrites({ repo: alice.did, swapCommit: head.root, @@ -790,18 +799,10 @@ describe('crud operations', () => { $type: `${ids.ComAtprotoRepoApplyWrites}#create`, action: 'create', collection: ids.AppBskyFeedPost, - rkey, value: { $type: ids.AppBskyFeedPost, ...postRecord() }, }, ], }) - const uri = AtUri.make(alice.did, ids.AppBskyFeedPost, rkey) - const checkPost = repo.getRecord({ - repo: uri.host, - collection: uri.collection, - rkey: uri.rkey, - }) - await expect(checkPost).resolves.toBeDefined() }) it('applyWrites fails on bad commit cas', async () => { @@ -821,7 +822,6 @@ describe('crud operations', () => { $type: `${ids.ComAtprotoRepoApplyWrites}#create`, action: 'create', collection: ids.AppBskyFeedPost, - rkey: TID.nextStr(), value: { $type: ids.AppBskyFeedPost, ...postRecord() }, }, ], diff --git a/packages/pds/tests/event-stream/__snapshots__/sync.test.ts.snap b/packages/pds/tests/event-stream/__snapshots__/sync.test.ts.snap deleted file mode 100644 index 9b349f85259..00000000000 --- a/packages/pds/tests/event-stream/__snapshots__/sync.test.ts.snap +++ /dev/null @@ -1,1224 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`sync rebuilds timeline indexes from repo state. 1`] = ` -Array [ - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(1)", - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(1)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(2)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(5)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(6)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(3)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(4)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(5)", - "uri": "record(6)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - "reason": Object { - "$type": "app.bsky.feed.defs#reasonRepost", - "by": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(6)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(7)", - "uri": "record(10)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "thanks bob", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(9)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(7)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(10)", - "val": "test-label", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(10)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(10)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(8)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "reply": Object { - "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "of course", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(11)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(7)", - "embed": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - ], - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(10)", - "val": "test-label", - }, - Object { - "cid": "cids(7)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(10)", - "val": "test-label-2", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 4114, - }, - }, - ], - }, - "reply": Object { - "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - "root": Object { - "cid": "cids(0)", - "uri": "record(0)", - }, - }, - "text": "hear that label_me label_me_2", - }, - "replyCount": 1, - "repostCount": 0, - "uri": "record(10)", - "viewer": Object {}, - }, - "reply": Object { - "parent": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - "root": Object { - "$type": "app.bsky.feed.defs#postView", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(9)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(1)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(2)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(3)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(4)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(5)", - "uri": "record(6)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(2)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(9)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(12)", - "val": "test-label", - }, - ], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(1)", - "uri": "record(2)", - }, - }, - "text": "yoohoo label_me", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(12)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(10)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000+00:00", - "text": "bobby boy here", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(13)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(0)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 3, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", - }, - "replyCount": 2, - "repostCount": 1, - "uri": "record(0)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(1)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(2)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(5)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(6)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - ], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(3)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(4)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(5)", - "uri": "record(6)", - }, - }, - }, - "text": "hi im carol", - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(2)", - "uri": "record(3)", - }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, - }, - ], - "text": "@alice.bluesky.xyz is the best", - }, - "replyCount": 0, - "repostCount": 1, - "uri": "record(2)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(1)", - "muted": false, - }, - }, - "cid": "cids(11)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "dan here!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(14)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", - "muted": false, - }, - }, - "cid": "cids(2)", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - "thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg", - }, - ], - }, - "record": Object { - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(5)", - "embeds": Array [], - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "uri": "record(6)", - "value": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - }, - }, - }, - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 2, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.recordWithMedia", - "media": Object { - "$type": "app.bsky.embed.images", - "images": Array [ - Object { - "alt": "tests/image/fixtures/key-landscape-small.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(3)", - }, - "size": 4114, - }, - }, - Object { - "alt": "tests/image/fixtures/key-alt.jpg", - "image": Object { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": Object { - "$link": "cids(4)", - }, - "size": 12736, - }, - }, - ], - }, - "record": Object { - "record": Object { - "cid": "cids(5)", - "uri": "record(6)", - }, - }, - }, - "text": "hi im carol", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(3)", - "viewer": Object { - "like": "record(15)", - }, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", - "displayName": "bobby", - "handle": "bob.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", - "muted": false, - }, - }, - "cid": "cids(5)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(6)", - "viewer": Object {}, - }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(0)", - "displayName": "ali", - "handle": "alice.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "muted": false, - }, - }, - "cid": "cids(12)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "hey there", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(16)", - "viewer": Object {}, - }, - }, -] -`; diff --git a/packages/pds/tests/event-stream/sync.test.ts b/packages/pds/tests/event-stream/sync.test.ts deleted file mode 100644 index a596b7240ce..00000000000 --- a/packages/pds/tests/event-stream/sync.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { sql } from 'kysely' -import AtpAgent from '@atproto/api' -import { getWriteLog, RecordWriteOp, WriteOpAction } from '@atproto/repo' -import SqlRepoStorage from '../../src/sql-repo-storage' -import basicSeed from '../seeds/basic' -import { SeedClient } from '../seeds/client' -import { forSnapshot, runTestServer, TestServerInfo } from '../_util' -import { - prepareCreate, - prepareDelete, - prepareUpdate, - PreparedWrite, -} from '../../src/repo' -import { AppContext } from '../../src' - -describe('sync', () => { - let server: TestServerInfo - let ctx: AppContext - let agent: AtpAgent - let sc: SeedClient - - beforeAll(async () => { - server = await runTestServer({ - dbPostgresSchema: 'event_stream_sync', - }) - ctx = server.ctx - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await basicSeed(sc) - }) - - afterAll(async () => { - await server.close() - }) - - it('rebuilds timeline indexes from repo state.', async () => { - const { db, services } = ctx - const { ref } = db.db.dynamic - // Destroy indexes - await Promise.all( - indexedTables.map((t) => sql`delete from ${ref(t)}`.execute(db.db)), - ) - // Confirm timeline empty - const emptiedTL = await agent.api.app.bsky.feed.getTimeline( - {}, - { headers: sc.getHeaders(sc.dids.alice) }, - ) - expect(emptiedTL.data.feed).toEqual([]) - // Compute oplog from state of all repos - const repoOpLogs = await Promise.all( - Object.entries(sc.dids) - .sort(([nameA], [nameB]) => nameA.localeCompare(nameB)) // Order for test determinism - .map(([, did]) => did) - .map(async (did) => ({ - did, - opLog: await getOpLog(did), - })), - ) - const indexOps = repoOpLogs.flatMap(({ did, opLog }) => - opLog.map((ops) => ({ did, ops })), - ) - // Run oplog through indexers - let ts = Date.now() // Increment for test determinism - for (const op of indexOps) { - const now = new Date(ts++).toISOString() - const writes = await prepareWrites(op.did, op.ops) - await db.transaction((dbTxn) => - services.repo(dbTxn).indexWrites(writes, now), - ) - } - await server.processAll() - // Check indexed timeline - const aliceTL = await agent.api.app.bsky.feed.getTimeline( - {}, - { headers: sc.getHeaders(sc.dids.alice) }, - ) - expect(forSnapshot(aliceTL.data.feed)).toMatchSnapshot() - }) - - async function getOpLog(did: string) { - const { db } = ctx - const storage = new SqlRepoStorage(db, did) - const root = await storage.getHead() - if (!root) throw new Error('Missing repo root') - return await getWriteLog(storage, root, null) - } - - function prepareWrites( - did: string, - ops: RecordWriteOp[], - ): Promise { - return Promise.all( - ops.map((op) => { - const { action } = op - if (action === WriteOpAction.Create) { - return prepareCreate({ - did, - collection: op.collection, - rkey: op.rkey, - record: op.record, - }) - } else if (action === WriteOpAction.Update) { - return prepareUpdate({ - did, - collection: op.collection, - rkey: op.rkey, - record: op.record, - }) - } else if (action === WriteOpAction.Delete) { - return prepareDelete({ - did, - collection: op.collection, - rkey: op.rkey, - }) - } else { - const exhaustiveCheck: never = action - throw new Error(`Unhandled case: ${exhaustiveCheck}`) - } - }), - ) - } - - const indexedTables = [ - 'record', - 'duplicate_record', - 'user_notification', - 'profile', - 'follow', - 'post', - 'post_hierarchy', - 'post_embed_image', - 'post_embed_external', - 'post_embed_record', - 'repost', - 'feed_item', - 'like', - /* Not these: - * 'ipld_block', - * 'blob', - * 'repo_blob', - * 'user', - * 'did_handle', - * 'refresh_token', - * 'repo_root', - * 'invite_code', - * 'invite_code_use', - * 'message_queue', - * 'message_queue_cursor', - */ - ] -}) diff --git a/packages/pds/tests/indexing.test.ts b/packages/pds/tests/indexing.test.ts index 4aede170617..7c2c7362079 100644 --- a/packages/pds/tests/indexing.test.ts +++ b/packages/pds/tests/indexing.test.ts @@ -51,27 +51,6 @@ describe('indexing', () => { } as AppBskyFeedPost.Record, }) const { uri } = createRecord - const updateRecord = await prepareUpdate({ - did: sc.dids.alice, - collection: ids.AppBskyFeedPost, - rkey: uri.rkey, - record: { - $type: ids.AppBskyFeedPost, - text: '@carol.test how are you?', - facets: [ - { - index: { byteStart: 0, byteEnd: 11 }, - features: [ - { - $type: `${ids.AppBskyRichtextFacet}#mention`, - did: sc.dids.carol, - }, - ], - }, - ], - createdAt, - } as AppBskyFeedPost.Record, - }) const deleteRecord = prepareDelete({ did: sc.dids.alice, collection: ids.AppBskyFeedPost, @@ -91,19 +70,6 @@ describe('indexing', () => { expect(forSnapshot(getAfterCreate.data)).toMatchSnapshot() const createNotifications = await getNotifications(db, uri) - // Update - await services - .repo(db) - .processWrites({ did: sc.dids.alice, writes: [updateRecord] }, 1) - await server.processAll() - - const getAfterUpdate = await agent.api.app.bsky.feed.getPostThread( - { uri: uri.toString() }, - { headers: sc.getHeaders(sc.dids.alice) }, - ) - expect(forSnapshot(getAfterUpdate.data)).toMatchSnapshot() - const updateNotifications = await getNotifications(db, uri) - // Delete await services .repo(db) @@ -120,7 +86,6 @@ describe('indexing', () => { expect( forSnapshot({ createNotifications, - updateNotifications, deleteNotifications, }), ).toMatchSnapshot() diff --git a/packages/pds/tests/races.test.ts b/packages/pds/tests/races.test.ts index b30ac561621..dad41a0d8ae 100644 --- a/packages/pds/tests/races.test.ts +++ b/packages/pds/tests/races.test.ts @@ -2,7 +2,7 @@ import AtpAgent from '@atproto/api' import { CloseFn, runTestServer } from './_util' import AppContext from '../src/context' import { PreparedWrite, prepareCreate } from '../src/repo' -import { TID, wait } from '@atproto/common' +import { wait } from '@atproto/common' import SqlRepoStorage from '../src/sql-repo-storage' import { CommitData, MemoryBlockstore, loadFullRepo } from '@atproto/repo' import { ConcurrentWriteError } from '../src/services/repo' @@ -40,7 +40,6 @@ describe('crud operations', () => { text: 'one', createdAt: new Date().toISOString(), }, - rkey: TID.nextStr(), validate: true, }) const storage = new SqlRepoStorage(ctx.db, did) From acd5185984a750d4befbbfed5085c3d787cfab5b Mon Sep 17 00:00:00 2001 From: Patrick Linnane Date: Tue, 25 Jul 2023 23:43:05 -0400 Subject: [PATCH 065/237] Fix various typos (#1373) --- lexicons/com/atproto/moderation/defs.json | 2 +- packages/bsky/tests/db.test.ts | 2 +- packages/bsky/tests/labeler/hive.test.ts | 4 ++-- packages/bsky/tests/views/posts.test.ts | 4 ++-- packages/pds/tests/crud.test.ts | 8 ++++---- packages/pds/tests/db.test.ts | 2 +- packages/pds/tests/invite-codes.test.ts | 2 +- packages/pds/tests/labeler/hive.test.ts | 4 ++-- packages/pds/tests/views/posts.test.ts | 4 ++-- packages/xrpc-server/src/types.ts | 4 ++-- packages/xrpc/src/types.ts | 6 +++--- 11 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lexicons/com/atproto/moderation/defs.json b/lexicons/com/atproto/moderation/defs.json index 987771f0ca5..3d4e5455565 100644 --- a/lexicons/com/atproto/moderation/defs.json +++ b/lexicons/com/atproto/moderation/defs.json @@ -27,7 +27,7 @@ }, "reasonSexual": { "type": "token", - "description": "Unwanted or mis-labeled sexual content" + "description": "Unwanted or mislabeled sexual content" }, "reasonRude": { "type": "token", diff --git a/packages/bsky/tests/db.test.ts b/packages/bsky/tests/db.test.ts index dcb28efe6cc..26a80be69c4 100644 --- a/packages/bsky/tests/db.test.ts +++ b/packages/bsky/tests/db.test.ts @@ -126,7 +126,7 @@ describe('db', () => { expect(res.length).toBe(0) }) - it('ensures all inflight querys are rolled back', async () => { + it('ensures all inflight queries are rolled back', async () => { let promise: Promise | undefined = undefined const names: string[] = [] try { diff --git a/packages/bsky/tests/labeler/hive.test.ts b/packages/bsky/tests/labeler/hive.test.ts index 30a41d1a44b..3213d794e30 100644 --- a/packages/bsky/tests/labeler/hive.test.ts +++ b/packages/bsky/tests/labeler/hive.test.ts @@ -6,8 +6,8 @@ describe('labeling', () => { const exampleRespBytes = await fs.readFile( 'tests/labeler/fixtures/hiveai_resp_example.json', ) - const exmapleResp = JSON.parse(exampleRespBytes.toString()) - const classes = hive.respToClasses(exmapleResp) + const exampleResp = JSON.parse(exampleRespBytes.toString()) + const classes = hive.respToClasses(exampleResp) expect(classes.length).toBeGreaterThan(10) const labels = hive.summarizeLabels(classes) diff --git a/packages/bsky/tests/views/posts.test.ts b/packages/bsky/tests/views/posts.test.ts index 9eeb2a7a962..676d03ccd59 100644 --- a/packages/bsky/tests/views/posts.test.ts +++ b/packages/bsky/tests/views/posts.test.ts @@ -76,11 +76,11 @@ describe('pds posts views', () => { const posts = await agent.api.app.bsky.feed.getPosts({ uris }) expect(posts.data.posts.length).toBe(2) - const recivedUris = posts.data.posts.map((p) => p.uri).sort() + const receivedUris = posts.data.posts.map((p) => p.uri).sort() const expected = [ sc.posts[sc.dids.alice][0].ref.uriStr, sc.posts[sc.dids.bob][0].ref.uriStr, ].sort() - expect(recivedUris).toEqual(expected) + expect(receivedUris).toEqual(expected) }) }) diff --git a/packages/pds/tests/crud.test.ts b/packages/pds/tests/crud.test.ts index bf437390a14..d7c4d094a36 100644 --- a/packages/pds/tests/crud.test.ts +++ b/packages/pds/tests/crud.test.ts @@ -631,13 +631,13 @@ describe('crud operations', () => { record: postRecord(), }) const uri = new AtUri(post.uri) - const attempDelete = repo.deleteRecord({ + const attemptDelete = repo.deleteRecord({ repo: uri.host, collection: uri.collection, rkey: uri.rkey, swapCommit: staleHead.root, }) - await expect(attempDelete).rejects.toThrow(deleteRecord.InvalidSwapError) + await expect(attemptDelete).rejects.toThrow(deleteRecord.InvalidSwapError) const checkPost = repo.getRecord({ repo: uri.host, collection: uri.collection, @@ -676,13 +676,13 @@ describe('crud operations', () => { record: postRecord(), }) const uri = new AtUri(post.uri) - const attempDelete = repo.deleteRecord({ + const attemptDelete = repo.deleteRecord({ repo: uri.host, collection: uri.collection, rkey: uri.rkey, swapRecord: (await cidForCbor({})).toString(), }) - await expect(attempDelete).rejects.toThrow(deleteRecord.InvalidSwapError) + await expect(attemptDelete).rejects.toThrow(deleteRecord.InvalidSwapError) const checkPost = repo.getRecord({ repo: uri.host, collection: uri.collection, diff --git a/packages/pds/tests/db.test.ts b/packages/pds/tests/db.test.ts index ade4de3fa3f..1d9501f1d58 100644 --- a/packages/pds/tests/db.test.ts +++ b/packages/pds/tests/db.test.ts @@ -130,7 +130,7 @@ describe('db', () => { expect(res.length).toBe(0) }) - it('ensures all inflight querys are rolled back', async () => { + it('ensures all inflight queries are rolled back', async () => { let promise: Promise | undefined = undefined const names: string[] = [] try { diff --git a/packages/pds/tests/invite-codes.test.ts b/packages/pds/tests/invite-codes.test.ts index e9c34fcc800..e5abe1cdab6 100644 --- a/packages/pds/tests/invite-codes.test.ts +++ b/packages/pds/tests/invite-codes.test.ts @@ -150,7 +150,7 @@ describe('account', () => { expect(res3.data.codes.length).toBe(2) }) - it('admin gifted codes to not impact a users avilable codes', async () => { + it('admin gifted codes to not impact a users available codes', async () => { const account = await makeLoggedInAccount(agent) // again, pretend account was made 2 days ago diff --git a/packages/pds/tests/labeler/hive.test.ts b/packages/pds/tests/labeler/hive.test.ts index 30a41d1a44b..3213d794e30 100644 --- a/packages/pds/tests/labeler/hive.test.ts +++ b/packages/pds/tests/labeler/hive.test.ts @@ -6,8 +6,8 @@ describe('labeling', () => { const exampleRespBytes = await fs.readFile( 'tests/labeler/fixtures/hiveai_resp_example.json', ) - const exmapleResp = JSON.parse(exampleRespBytes.toString()) - const classes = hive.respToClasses(exmapleResp) + const exampleResp = JSON.parse(exampleRespBytes.toString()) + const classes = hive.respToClasses(exampleResp) expect(classes.length).toBeGreaterThan(10) const labels = hive.summarizeLabels(classes) diff --git a/packages/pds/tests/views/posts.test.ts b/packages/pds/tests/views/posts.test.ts index 7bf6e6919cf..f6aa0551b07 100644 --- a/packages/pds/tests/views/posts.test.ts +++ b/packages/pds/tests/views/posts.test.ts @@ -55,11 +55,11 @@ describe('pds posts views', () => { ) expect(posts.data.posts.length).toBe(2) - const recivedUris = posts.data.posts.map((p) => p.uri).sort() + const receivedUris = posts.data.posts.map((p) => p.uri).sort() const expected = [ sc.posts[sc.dids.alice][0].ref.uriStr, sc.posts[sc.dids.bob][0].ref.uriStr, ].sort() - expect(recivedUris).toEqual(expected) + expect(receivedUris).toEqual(expected) }) }) diff --git a/packages/xrpc-server/src/types.ts b/packages/xrpc-server/src/types.ts index 58c93339f0b..266a8db222a 100644 --- a/packages/xrpc-server/src/types.ts +++ b/packages/xrpc-server/src/types.ts @@ -166,9 +166,9 @@ export class UpstreamFailureError extends XRPCError { } } -export class NotEnoughResoucesError extends XRPCError { +export class NotEnoughResourcesError extends XRPCError { constructor(errorMessage?: string, customErrorName?: string) { - super(ResponseType.NotEnoughResouces, errorMessage, customErrorName) + super(ResponseType.NotEnoughResources, errorMessage, customErrorName) } } diff --git a/packages/xrpc/src/types.ts b/packages/xrpc/src/types.ts index e803e452b6e..181aa9b9264 100644 --- a/packages/xrpc/src/types.ts +++ b/packages/xrpc/src/types.ts @@ -41,7 +41,7 @@ export enum ResponseType { InternalServerError = 500, MethodNotImplemented = 501, UpstreamFailure = 502, - NotEnoughResouces = 503, + NotEnoughResources = 503, UpstreamTimeout = 504, } @@ -57,7 +57,7 @@ export const ResponseTypeNames = { [ResponseType.InternalServerError]: 'InternalServerError', [ResponseType.MethodNotImplemented]: 'MethodNotImplemented', [ResponseType.UpstreamFailure]: 'UpstreamFailure', - [ResponseType.NotEnoughResouces]: 'NotEnoughResouces', + [ResponseType.NotEnoughResources]: 'NotEnoughResources', [ResponseType.UpstreamTimeout]: 'UpstreamTimeout', } @@ -73,7 +73,7 @@ export const ResponseTypeStrings = { [ResponseType.InternalServerError]: 'Internal Server Error', [ResponseType.MethodNotImplemented]: 'Method Not Implemented', [ResponseType.UpstreamFailure]: 'Upstream Failure', - [ResponseType.NotEnoughResouces]: 'Not Enough Resouces', + [ResponseType.NotEnoughResources]: 'Not Enough Resources', [ResponseType.UpstreamTimeout]: 'Upstream Timeout', } From ca7b891ece9a91a1e3888f716146c5a3c7521439 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 25 Jul 2023 22:43:15 -0500 Subject: [PATCH 066/237] Log injection (#1384) datadog inject logs --- packages/bsky/service/index.js | 3 ++- packages/pds/service/index.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/bsky/service/index.js b/packages/bsky/service/index.js index f7b4180a8a5..85cdc832146 100644 --- a/packages/bsky/service/index.js +++ b/packages/bsky/service/index.js @@ -1,6 +1,7 @@ 'use strict' /* eslint-disable */ -require('dd-trace/init') // Only works with commonjs +require('dd-trace') // Only works with commonjs + .init({ logInjection: true }) .tracer.use('express', { hooks: { request: (span, req) => { diff --git a/packages/pds/service/index.js b/packages/pds/service/index.js index 77f20b5d84e..bf42eac99e3 100644 --- a/packages/pds/service/index.js +++ b/packages/pds/service/index.js @@ -1,6 +1,7 @@ 'use strict' /* eslint-disable */ -require('dd-trace/init') // Only works with commonjs +require('dd-trace') // Only works with commonjs + .init({ logInjection: true }) .tracer.use('express', { hooks: { request: (span, req) => { From 7c20b012725307a16dbf8ce82465664991f87a07 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 27 Jul 2023 13:10:14 -0500 Subject: [PATCH 067/237] Proxy writes to appview (#1392) * proxy writes to appview * dont build branch --- .../pds/src/app-view/api/app/bsky/graph/muteActor.ts | 10 +++++----- .../src/app-view/api/app/bsky/graph/muteActorList.ts | 10 +++++----- .../src/app-view/api/app/bsky/graph/unmuteActor.ts | 10 +++++----- .../app-view/api/app/bsky/graph/unmuteActorList.ts | 10 +++++----- .../app-view/api/app/bsky/notification/updateSeen.ts | 12 ++++++------ packages/pds/src/context.ts | 3 ++- 6 files changed, 28 insertions(+), 27 deletions(-) diff --git a/packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts b/packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts index ac22ebd2bba..4cae0c45e27 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/muteActor.ts @@ -18,17 +18,17 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError('Cannot mute oneself') } - await services.account(db).mute({ - did: subject.did, - mutedByDid: requester, - }) - if (ctx.canProxyWrite()) { await ctx.appviewAgent.api.app.bsky.graph.muteActor(input.body, { ...(await ctx.serviceAuthHeaders(requester)), encoding: 'application/json', }) } + + await services.account(db).mute({ + did: subject.did, + mutedByDid: requester, + }) }, }) } diff --git a/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts b/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts index 65c09125f45..ba41fe8c52e 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts @@ -17,17 +17,17 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Invalid collection: expected: ${collId}`) } - await ctx.services.account(ctx.db).muteActorList({ - list, - mutedByDid: requester, - }) - if (ctx.canProxyWrite()) { await ctx.appviewAgent.api.app.bsky.graph.muteActorList(input.body, { ...(await ctx.serviceAuthHeaders(requester)), encoding: 'application/json', }) } + + await ctx.services.account(ctx.db).muteActorList({ + list, + mutedByDid: requester, + }) }, }) } diff --git a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts b/packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts index 1b911e71336..ed9585a60b5 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/unmuteActor.ts @@ -15,17 +15,17 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Actor not found: ${actor}`) } - await services.account(db).unmute({ - did: subject.did, - mutedByDid: requester, - }) - if (ctx.canProxyWrite()) { await ctx.appviewAgent.api.app.bsky.graph.unmuteActor(input.body, { ...(await ctx.serviceAuthHeaders(requester)), encoding: 'application/json', }) } + + await services.account(db).unmute({ + did: subject.did, + mutedByDid: requester, + }) }, }) } diff --git a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts b/packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts index 36e65e4178a..6141bd619bc 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/unmuteActorList.ts @@ -8,17 +8,17 @@ export default function (server: Server, ctx: AppContext) { const { list } = input.body const requester = auth.credentials.did - await ctx.services.account(ctx.db).unmuteActorList({ - list, - mutedByDid: requester, - }) - if (ctx.canProxyWrite()) { await ctx.appviewAgent.api.app.bsky.graph.unmuteActorList(input.body, { ...(await ctx.serviceAuthHeaders(requester)), encoding: 'application/json', }) } + + await ctx.services.account(ctx.db).unmuteActorList({ + list, + mutedByDid: requester, + }) }, }) } diff --git a/packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts b/packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts index f5644abfe7f..0524a1e8581 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts +++ b/packages/pds/src/app-view/api/app/bsky/notification/updateSeen.ts @@ -21,12 +21,6 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Could not find user: ${requester}`) } - await ctx.db.db - .updateTable('user_state') - .set({ lastSeenNotifs: parsed }) - .where('did', '=', user.did) - .executeTakeFirst() - if (ctx.canProxyWrite()) { await ctx.appviewAgent.api.app.bsky.notification.updateSeen( input.body, @@ -36,6 +30,12 @@ export default function (server: Server, ctx: AppContext) { }, ) } + + await ctx.db.db + .updateTable('user_state') + .set({ lastSeenNotifs: parsed }) + .where('did', '=', user.did) + .executeTakeFirst() }, }) } diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 910699bfd2b..07ffdee5a1f 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -209,7 +209,8 @@ export class AppContext { canProxyWrite(): boolean { return ( - this.cfg.bskyAppViewProxy && this.cfg.bskyAppViewEndpoint !== undefined + this.cfg.bskyAppViewEndpoint !== undefined && + this.cfg.bskyAppViewDid !== undefined ) } } From 030be22f602dde85c9a1288fb80319cde24d4641 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 27 Jul 2023 18:49:09 -0500 Subject: [PATCH 068/237] Adjust bsky team feed (#1394) * adjust bsky team feed * tweak --- packages/bsky/src/feed-gen/bsky-team.ts | 11 +---------- packages/pds/src/feed-gen/bsky-team.ts | 11 +---------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/packages/bsky/src/feed-gen/bsky-team.ts b/packages/bsky/src/feed-gen/bsky-team.ts index 8655ab5af2e..537a259cd31 100644 --- a/packages/bsky/src/feed-gen/bsky-team.ts +++ b/packages/bsky/src/feed-gen/bsky-team.ts @@ -6,18 +6,9 @@ import { AlgoHandler, AlgoResponse } from './types' import { FeedKeyset } from '../api/app/bsky/util/feed' const BSKY_TEAM: NotEmptyArray = [ - 'did:plc:oky5czdrnfjpqslsw2a5iclo', // jay - 'did:plc:yk4dd2qkboz2yv6tpubpc6co', // daniel - 'did:plc:ragtjsm2j2vknwkz3zp4oxrd', // paul - 'did:plc:l3rouwludahu3ui3bt66mfvj', // devin - 'did:plc:tpg43qhh4lw4ksiffs4nbda3', // jake - 'did:plc:44ybard66vv44zksje25o7dz', // bryan - 'did:plc:qjeavhlw222ppsre4rscd3n2', // rose - 'did:plc:vjug55kidv6sye7ykr5faxxn', // emily - 'did:plc:fgsn4gf2dlgnybo4nbej5b2s', // ansh - 'did:plc:vpkhqolt662uhesyj6nxm7ys', // why 'did:plc:z72i7hdynmk6r22z27h6tvur', // @bsky.app 'did:plc:ewvi7nxzyoun6zhxrhs64oiz', // @atproto.com + 'did:plc:eon2iu7v3x2ukgxkqaf7e5np', // @safety.bsky.app ] const handler: AlgoHandler = async ( diff --git a/packages/pds/src/feed-gen/bsky-team.ts b/packages/pds/src/feed-gen/bsky-team.ts index fa8224a4b7d..ba4196e79d9 100644 --- a/packages/pds/src/feed-gen/bsky-team.ts +++ b/packages/pds/src/feed-gen/bsky-team.ts @@ -6,18 +6,9 @@ import { AlgoHandler, AlgoResponse } from './types' import { FeedKeyset } from '../app-view/api/app/bsky/util/feed' const BSKY_TEAM: NotEmptyArray = [ - 'did:plc:oky5czdrnfjpqslsw2a5iclo', // jay - 'did:plc:yk4dd2qkboz2yv6tpubpc6co', // daniel - 'did:plc:ragtjsm2j2vknwkz3zp4oxrd', // paul - 'did:plc:l3rouwludahu3ui3bt66mfvj', // devin - 'did:plc:tpg43qhh4lw4ksiffs4nbda3', // jake - 'did:plc:44ybard66vv44zksje25o7dz', // bryan - 'did:plc:qjeavhlw222ppsre4rscd3n2', // rose - 'did:plc:vjug55kidv6sye7ykr5faxxn', // emily - 'did:plc:fgsn4gf2dlgnybo4nbej5b2s', // ansh - 'did:plc:vpkhqolt662uhesyj6nxm7ys', // why 'did:plc:z72i7hdynmk6r22z27h6tvur', // @bsky.app 'did:plc:ewvi7nxzyoun6zhxrhs64oiz', // @atproto.com + 'did:plc:eon2iu7v3x2ukgxkqaf7e5np', // @safety.bsky.app ] const handler: AlgoHandler = async ( From f293366e6a2fce1098291c1c1c7504b67b804ff5 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 27 Jul 2023 19:03:06 -0500 Subject: [PATCH 069/237] Tweak timeline construction (#1367) * add idx to post table & tweak feed construction * comment out migration & build branch * split out queries * couple tweaks * bugfix in with-friends * lower feed data threshold * return to subquery * log injection on tracing * Tweak timeline migraiton (#1369) * v0.4.3 * tweak timeline migraiton * misspelling * move mutes & blocks to pds * moar tweaks * joins with 1 day cutoff * no blocks/mutes on with friends * attempt forcing custom plan * increase feed date threshold * trying something out * trying something more * tidy * lower timeline date threshold * tidy --- .../app/bsky/unspecced/getTimelineSkeleton.ts | 76 +++++++++++-------- .../20230720T164800037Z-posts-cursor-idx.ts | 19 +++++ packages/bsky/src/db/migrations/index.ts | 1 + packages/bsky/src/feed-gen/with-friends.ts | 13 +--- packages/common/src/ipld.ts | 9 +++ 5 files changed, 76 insertions(+), 42 deletions(-) create mode 100644 packages/bsky/src/db/migrations/20230720T164800037Z-posts-cursor-idx.ts diff --git a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts index 0684ce91144..c86e7433bc6 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts @@ -14,50 +14,64 @@ export default function (server: Server, ctx: AppContext) { const db = ctx.db.db const { ref } = db.dynamic - const feedService = ctx.services.feed(ctx.db) - const graphService = ctx.services.graph(ctx.db) - - const followingIdsSubquery = db - .selectFrom('follow') - .select('follow.subjectDid') - .where('follow.creator', '=', viewer) - const keyset = new FeedKeyset( ref('feed_item.sortAt'), ref('feed_item.cid'), ) const sortFrom = keyset.unpack(cursor)?.primary - let feedItemsQb = feedService - .selectFeedItemQb() - .where((qb) => - qb - .where('originatorDid', '=', viewer) - .orWhere('originatorDid', 'in', followingIdsSubquery), - ) - .where((qb) => - // Hide posts and reposts of or by muted actors - graphService.whereNotMuted(qb, viewer, [ - ref('post.creator'), - ref('originatorDid'), - ]), - ) - .whereNotExists( - graphService.blockQb(viewer, [ - ref('post.creator'), - ref('originatorDid'), - ]), - ) - .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom)) + let followQb = db + .selectFrom('feed_item') + .innerJoin('follow', 'follow.subjectDid', 'feed_item.originatorDid') + .where('follow.creator', '=', viewer) + .innerJoin('post', 'post.uri', 'feed_item.postUri') + .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom, 1)) + .selectAll('feed_item') + .select([ + 'post.replyRoot', + 'post.replyParent', + 'post.creator as postAuthorDid', + ]) - feedItemsQb = paginate(feedItemsQb, { + followQb = paginate(followQb, { limit, cursor, keyset, tryIndex: true, }) - const feedItems = await feedItemsQb.execute() + let selfQb = ctx.db.db + .selectFrom('feed_item') + .innerJoin('post', 'post.uri', 'feed_item.postUri') + .where('feed_item.originatorDid', '=', viewer) + .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom, 1)) + .selectAll('feed_item') + .select([ + 'post.replyRoot', + 'post.replyParent', + 'post.creator as postAuthorDid', + ]) + + selfQb = paginate(selfQb, { + limit: Math.min(limit, 10), + cursor, + keyset, + tryIndex: true, + }) + + const [followRes, selfRes] = await Promise.all([ + followQb.execute(), + selfQb.execute(), + ]) + + const feedItems = [...followRes, ...selfRes] + .sort((a, b) => { + if (a.sortAt > b.sortAt) return -1 + if (a.sortAt < b.sortAt) return 1 + return a.cid > b.cid ? -1 : 1 + }) + .slice(0, limit) + const feed = feedItems.map((item) => ({ post: item.postUri, reason: diff --git a/packages/bsky/src/db/migrations/20230720T164800037Z-posts-cursor-idx.ts b/packages/bsky/src/db/migrations/20230720T164800037Z-posts-cursor-idx.ts new file mode 100644 index 00000000000..4bf7539b1c3 --- /dev/null +++ b/packages/bsky/src/db/migrations/20230720T164800037Z-posts-cursor-idx.ts @@ -0,0 +1,19 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createIndex('post_creator_cursor_idx') + .on('post') + .columns(['creator', 'sortAt', 'cid']) + .execute() + await db.schema.dropIndex('post_creator_idx').execute() +} + +export async function down(db: Kysely): Promise { + await db.schema + .createIndex('post_creator_idx') + .on('post') + .column('creator') + .execute() + await db.schema.dropIndex('post_creator_cursor_idx').execute() +} diff --git a/packages/bsky/src/db/migrations/index.ts b/packages/bsky/src/db/migrations/index.ts index 6d29ffe6576..daa87e0e1d8 100644 --- a/packages/bsky/src/db/migrations/index.ts +++ b/packages/bsky/src/db/migrations/index.ts @@ -20,3 +20,4 @@ export * as _20230620T161134972Z from './20230620T161134972Z-post-langs' export * as _20230627T212437895Z from './20230627T212437895Z-optional-handle' export * as _20230629T220835893Z from './20230629T220835893Z-remove-post-hierarchy' export * as _20230703T045536691Z from './20230703T045536691Z-feed-and-label-indices' +export * as _20230720T164800037Z from './20230720T164800037Z-posts-cursor-idx' diff --git a/packages/bsky/src/feed-gen/with-friends.ts b/packages/bsky/src/feed-gen/with-friends.ts index 6817116f40f..0c53f9f4bc4 100644 --- a/packages/bsky/src/feed-gen/with-friends.ts +++ b/packages/bsky/src/feed-gen/with-friends.ts @@ -11,7 +11,6 @@ const handler: AlgoHandler = async ( ): Promise => { const { cursor, limit = 50 } = params const feedService = ctx.services.feed(ctx.db) - const graphService = ctx.services.graph(ctx.db) const { ref } = ctx.db.db.dynamic @@ -20,18 +19,10 @@ const handler: AlgoHandler = async ( let postsQb = feedService .selectPostQb() + .innerJoin('follow', 'follow.subjectDid', 'post.creator') .innerJoin('post_agg', 'post_agg.uri', 'post.uri') .where('post_agg.likeCount', '>=', 5) - .whereExists((qb) => - qb - .selectFrom('follow') - .where('follow.creator', '=', requester) - .whereRef('follow.subjectDid', '=', 'post.creator'), - ) - .where((qb) => - graphService.whereNotMuted(qb, requester, [ref('post.creator')]), - ) - .whereNotExists(graphService.blockQb(requester, [ref('post.creator')])) + .where('follow.creator', '=', requester) .where('post.indexedAt', '>', getFeedDateThreshold(sortFrom)) postsQb = paginate(postsQb, { limit, cursor, keyset, tryIndex: true }) diff --git a/packages/common/src/ipld.ts b/packages/common/src/ipld.ts index bea8e5f4e81..6939afe1049 100644 --- a/packages/common/src/ipld.ts +++ b/packages/common/src/ipld.ts @@ -24,6 +24,15 @@ export const cidForCbor = async (data: unknown): Promise => { return block.cid } +export const isValidCid = async (cidStr: string): Promise => { + try { + const parsed = CID.parse(cidStr) + return parsed.toString() === cidStr + } catch (err) { + return false + } +} + export const cborBytesToRecord = ( bytes: Uint8Array, ): Record => { From 7ef73467eb37433ca9e84f48788935467207ddee Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 27 Jul 2023 19:05:12 -0500 Subject: [PATCH 070/237] Remove local whats hot from pds (#1393) * put behind proxy header * toggle for everyone * tidy * tidy * fix --- packages/bsky/src/feed-gen/index.ts | 2 -- packages/bsky/tests/algos/whats-hot.test.ts | 2 +- .../src/app-view/api/app/bsky/feed/getFeed.ts | 21 +++---------------- packages/pds/src/feed-gen/index.ts | 2 -- packages/pds/tests/algos/hot-classic.test.ts | 2 +- packages/pds/tests/algos/whats-hot.test.ts | 2 +- 6 files changed, 6 insertions(+), 25 deletions(-) diff --git a/packages/bsky/src/feed-gen/index.ts b/packages/bsky/src/feed-gen/index.ts index 73edb2b93de..c05f8cb72d3 100644 --- a/packages/bsky/src/feed-gen/index.ts +++ b/packages/bsky/src/feed-gen/index.ts @@ -2,7 +2,6 @@ import { AtUri } from '@atproto/uri' import { ids } from '../lexicon/lexicons' import withFriends from './with-friends' import bskyTeam from './bsky-team' -import whatsHot from './whats-hot' import hotClassic from './hot-classic' import bestOfFollows from './best-of-follows' import mutuals from './mutuals' @@ -16,7 +15,6 @@ const feedgenUri = (did, name) => export const makeAlgos = (did: string): MountedAlgos => ({ [feedgenUri(did, 'with-friends')]: withFriends, [feedgenUri(did, 'bsky-team')]: bskyTeam, - [feedgenUri(did, 'whats-hot')]: whatsHot, [feedgenUri(did, 'hot-classic')]: hotClassic, [feedgenUri(did, 'best-of-follows')]: bestOfFollows, [feedgenUri(did, 'mutuals')]: mutuals, diff --git a/packages/bsky/tests/algos/whats-hot.test.ts b/packages/bsky/tests/algos/whats-hot.test.ts index 3f6ef48c29c..4a214bf76ab 100644 --- a/packages/bsky/tests/algos/whats-hot.test.ts +++ b/packages/bsky/tests/algos/whats-hot.test.ts @@ -5,7 +5,7 @@ import basicSeed from '../seeds/basic' import { makeAlgos } from '../../src' import { TestNetwork } from '@atproto/dev-env' -describe('algo whats-hot', () => { +describe.skip('algo whats-hot', () => { let network: TestNetwork let agent: AtpAgent let sc: SeedClient diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts index d1431342a08..8620f83b730 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts @@ -11,7 +11,7 @@ import { PoorlyFormattedDidDocumentError, getFeedGen, } from '@atproto/identity' -import { AtpAgent, AppBskyFeedGetFeedSkeleton, AtUri } from '@atproto/api' +import { AtpAgent, AppBskyFeedGetFeedSkeleton } from '@atproto/api' import { SkeletonFeedPost } from '../../../../../lexicon/types/app/bsky/feed/defs' import { QueryParams as GetFeedParams } from '../../../../../lexicon/types/app/bsky/feed/getFeed' import { OutputSchema as SkeletonOutput } from '../../../../../lexicon/types/app/bsky/feed/getFeedSkeleton' @@ -20,19 +20,9 @@ import AppContext from '../../../../../context' import { FeedRow } from '../../../../services/feed' import { AlgoResponse } from '../../../../../feed-gen/types' -// temp hardcoded feeds that we can proxy to appview -const PROXYABLE_FEEDS = [ - 'with-friends', - 'bsky-team', - 'hot-classic', - 'best-of-follows', - 'mutuals', -] - export default function (server: Server, ctx: AppContext) { const isProxyableFeed = (feed: string): boolean => { - const uri = new AtUri(feed) - return feed in ctx.algos && PROXYABLE_FEEDS.includes(uri.rkey) + return feed in ctx.algos } server.app.bsky.feed.getFeed({ @@ -71,12 +61,7 @@ export default function (server: Server, ctx: AppContext) { requester, ) } else { - const { feed } = params - const localAlgo = ctx.algos[feed] - algoRes = - localAlgo !== undefined - ? await localAlgo(ctx, params, requester) - : await skeletonFromFeedGen(ctx, params, requester) + algoRes = await skeletonFromFeedGen(ctx, params, requester) } timerSkele.stop() diff --git a/packages/pds/src/feed-gen/index.ts b/packages/pds/src/feed-gen/index.ts index 0d17c6544ae..038c59321b1 100644 --- a/packages/pds/src/feed-gen/index.ts +++ b/packages/pds/src/feed-gen/index.ts @@ -1,7 +1,6 @@ import { AtUri } from '@atproto/uri' import withFriends from './with-friends' import bskyTeam from './bsky-team' -import whatsHot from './whats-hot' import hotClassic from './hot-classic' import bestOfFollows from './best-of-follows' import mutuals from './mutuals' @@ -15,7 +14,6 @@ const coll = ids.AppBskyFeedGenerator export const makeAlgos = (did: string): MountedAlgos => ({ [AtUri.make(did, coll, 'with-friends').toString()]: withFriends, [AtUri.make(did, coll, 'bsky-team').toString()]: bskyTeam, - [AtUri.make(did, coll, 'whats-hot').toString()]: whatsHot, [AtUri.make(did, coll, 'hot-classic').toString()]: hotClassic, [AtUri.make(did, coll, 'best-of-follows').toString()]: bestOfFollows, [AtUri.make(did, coll, 'mutuals').toString()]: mutuals, diff --git a/packages/pds/tests/algos/hot-classic.test.ts b/packages/pds/tests/algos/hot-classic.test.ts index 1d5804f689a..82a65f5c852 100644 --- a/packages/pds/tests/algos/hot-classic.test.ts +++ b/packages/pds/tests/algos/hot-classic.test.ts @@ -4,7 +4,7 @@ import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' import { makeAlgos } from '../../src' -describe('algo hot-classic', () => { +describe.skip('algo hot-classic', () => { let server: TestServerInfo let agent: AtpAgent let sc: SeedClient diff --git a/packages/pds/tests/algos/whats-hot.test.ts b/packages/pds/tests/algos/whats-hot.test.ts index 0b0a292c530..9ad33621af4 100644 --- a/packages/pds/tests/algos/whats-hot.test.ts +++ b/packages/pds/tests/algos/whats-hot.test.ts @@ -5,7 +5,7 @@ import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' import { makeAlgos } from '../../src' -describe('algo whats-hot', () => { +describe.skip('algo whats-hot', () => { let server: TestServerInfo let agent: AtpAgent let sc: SeedClient From c060db24bdcf9378e261f50828d6ab88f7dbeb22 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Fri, 28 Jul 2023 17:46:32 -0400 Subject: [PATCH 071/237] Optimize com.atproto.sync.listRepos (#1391) * add index to support listRepos * disable contents of user-account-cursor-idx migration, build * ready for merge --------- Co-authored-by: dholms --- .../pds/src/api/com/atproto/sync/listRepos.ts | 15 +++++++++------ ...20230727T172043676Z-user-account-cursor-idx.ts | 13 +++++++++++++ packages/pds/src/db/migrations/index.ts | 1 + 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 packages/pds/src/db/migrations/20230727T172043676Z-user-account-cursor-idx.ts diff --git a/packages/pds/src/api/com/atproto/sync/listRepos.ts b/packages/pds/src/api/com/atproto/sync/listRepos.ts index 739e861934a..597b949449f 100644 --- a/packages/pds/src/api/com/atproto/sync/listRepos.ts +++ b/packages/pds/src/api/com/atproto/sync/listRepos.ts @@ -8,22 +8,25 @@ export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.listRepos(async ({ params }) => { const { limit, cursor } = params const { ref } = ctx.db.db.dynamic - const innerBuilder = ctx.db.db - .selectFrom('repo_root') - .innerJoin('user_account', 'user_account.did', 'repo_root.did') + let builder = ctx.db.db + .selectFrom('user_account') + .innerJoin('repo_root', 'repo_root.did', 'user_account.did') .where(notSoftDeletedClause(ref('repo_root'))) .select([ - 'repo_root.did as did', + 'user_account.did as did', 'repo_root.root as head', 'user_account.createdAt as createdAt', ]) - let builder = ctx.db.db.selectFrom(innerBuilder.as('repos')).selectAll() - const keyset = new TimeDidKeyset(ref('createdAt'), ref('did')) + const keyset = new TimeDidKeyset( + ref('user_account.createdAt'), + ref('user_account.did'), + ) builder = paginate(builder, { limit, cursor, keyset, direction: 'asc', + tryIndex: true, }) const res = await builder.execute() const repos = res.map((row) => ({ did: row.did, head: row.head })) diff --git a/packages/pds/src/db/migrations/20230727T172043676Z-user-account-cursor-idx.ts b/packages/pds/src/db/migrations/20230727T172043676Z-user-account-cursor-idx.ts new file mode 100644 index 00000000000..15f38eafd65 --- /dev/null +++ b/packages/pds/src/db/migrations/20230727T172043676Z-user-account-cursor-idx.ts @@ -0,0 +1,13 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createIndex('user_account_cursor_idx') + .on('user_account') + .columns(['createdAt', 'did']) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropIndex('user_account_cursor_idx').execute() +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index 11b2432b9c6..b116974aa82 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -55,3 +55,4 @@ export * as _20230530T213530067Z from './20230530T213530067Z-rebase-indices' export * as _20230605T235529700Z from './20230605T235529700Z-outgoing-repo-seq' export * as _20230703T044601833Z from './20230703T044601833Z-feed-and-label-indices' export * as _20230718T170914772Z from './20230718T170914772Z-sequencer-leader-sequence' +export * as _20230727T172043676Z from './20230727T172043676Z-user-account-cursor-idx' From 850265d7bda957f753c75b47ea174d9eeb892405 Mon Sep 17 00:00:00 2001 From: dumbmoron <136796770+dumbmoron@users.noreply.github.com> Date: Tue, 1 Aug 2023 00:39:53 +0200 Subject: [PATCH 072/237] pds: actually validate tlds (#1408) tlds have not been appropriately checked since 6b51ecb due to what appears to be a typo/oversight. this commit fixes it so that tlds are once again validated --- packages/pds/src/handle/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/src/handle/index.ts b/packages/pds/src/handle/index.ts index d847df1cd45..88e1c3fc38f 100644 --- a/packages/pds/src/handle/index.ts +++ b/packages/pds/src/handle/index.ts @@ -14,7 +14,7 @@ export const normalizeAndValidateHandle = async (opts: { // base formatting validation const handle = baseNormalizeAndValidate(opts.handle) // tld validation - if (!ident.isValidTld) { + if (!ident.isValidTld(handle)) { throw new InvalidRequestError( 'Handle TLD is invalid or disallowed', 'InvalidHandle', From 74274c597dfdaffe457db869951876e94bfabfd2 Mon Sep 17 00:00:00 2001 From: Alona Enraght-Moony Date: Tue, 1 Aug 2023 00:35:05 +0100 Subject: [PATCH 073/237] lexicons: Remove empty array to make roundtriping work (#1407) --- lexicons/com/atproto/admin/defs.json | 1 - 1 file changed, 1 deletion(-) diff --git a/lexicons/com/atproto/admin/defs.json b/lexicons/com/atproto/admin/defs.json index 8626a004267..03bbced34b6 100644 --- a/lexicons/com/atproto/admin/defs.json +++ b/lexicons/com/atproto/admin/defs.json @@ -304,7 +304,6 @@ }, "moderation": { "type": "object", - "required": [], "properties": { "currentAction": { "type": "ref", "ref": "#actionViewCurrent" } } From 1b0a5716ce72fae2a176876fe65d8ed4983834da Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 31 Jul 2023 18:43:04 -0500 Subject: [PATCH 074/237] Reflect takedowns in actor service (#1412) reflect takedowns in actor service --- packages/bsky/src/services/actor/views.ts | 35 +++++++++++++--- .../pds/src/app-view/services/actor/views.ts | 41 ++++++++++++++++--- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/packages/bsky/src/services/actor/views.ts b/packages/bsky/src/services/actor/views.ts index ecdc43627a5..4bf75bef500 100644 --- a/packages/bsky/src/services/actor/views.ts +++ b/packages/bsky/src/services/actor/views.ts @@ -6,7 +6,7 @@ import { ProfileViewBasic, } from '../../lexicon/types/app/bsky/actor/defs' import Database from '../../db' -import { noMatch } from '../../db/util' +import { noMatch, notSoftDeletedClause } from '../../db/util' import { Actor } from '../../db/tables/actor' import { ImageUriBuilder } from '../../image/uri' import { LabelService } from '../label' @@ -22,19 +22,23 @@ export class ActorViews { profileDetailed( result: ActorResult, viewer: string | null, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise profileDetailed( result: ActorResult[], viewer: string | null, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise async profileDetailed( result: ActorResult | ActorResult[], viewer: string | null, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise { const results = Array.isArray(result) ? result : [result] if (results.length === 0) return [] const { ref } = this.db.db.dynamic + const { skipLabels = false, includeSoftDeleted = false } = opts ?? {} const dids = results.map((r) => r.did) const profileInfosQb = this.db.db @@ -42,6 +46,9 @@ export class ActorViews { .where('actor.did', 'in', dids) .leftJoin('profile', 'profile.creator', 'actor.did') .leftJoin('profile_agg', 'profile_agg.did', 'actor.did') + .if(!includeSoftDeleted, (qb) => + qb.where(notSoftDeletedClause(ref('actor'))), + ) .select([ 'actor.did as did', 'profile.uri as profileUri', @@ -92,7 +99,7 @@ export class ActorViews { const [profileInfos, labels, listMutes] = await Promise.all([ profileInfosQb.execute(), - this.services.label(this.db).getLabelsForSubjects(dids), + this.services.label(this.db).getLabelsForSubjects(skipLabels ? [] : dids), this.getListMutes(dids, viewer), ]) @@ -144,22 +151,35 @@ export class ActorViews { return Array.isArray(result) ? views : views[0] } - profile(result: ActorResult, viewer: string | null): Promise - profile(result: ActorResult[], viewer: string | null): Promise + profile( + result: ActorResult, + viewer: string | null, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, + ): Promise + profile( + result: ActorResult[], + viewer: string | null, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, + ): Promise async profile( result: ActorResult | ActorResult[], viewer: string | null, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise { const results = Array.isArray(result) ? result : [result] if (results.length === 0) return [] const { ref } = this.db.db.dynamic + const { skipLabels = false, includeSoftDeleted = false } = opts ?? {} const dids = results.map((r) => r.did) const profileInfosQb = this.db.db .selectFrom('actor') .where('actor.did', 'in', dids) .leftJoin('profile', 'profile.creator', 'actor.did') + .if(!includeSoftDeleted, (qb) => + qb.where(notSoftDeletedClause(ref('actor'))), + ) .select([ 'actor.did as did', 'profile.uri as profileUri', @@ -206,7 +226,7 @@ export class ActorViews { const [profileInfos, labels, listMutes] = await Promise.all([ profileInfosQb.execute(), - this.services.label(this.db).getLabelsForSubjects(dids), + this.services.label(this.db).getLabelsForSubjects(skipLabels ? [] : dids), this.getListMutes(dids, viewer), ]) @@ -251,19 +271,22 @@ export class ActorViews { profileBasic( result: ActorResult, viewer: string | null, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise profileBasic( result: ActorResult[], viewer: string | null, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise async profileBasic( result: ActorResult | ActorResult[], viewer: string | null, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise { const results = Array.isArray(result) ? result : [result] if (results.length === 0) return [] - const profiles = await this.profile(results, viewer) + const profiles = await this.profile(results, viewer, opts) const views = profiles.map((view) => ({ did: view.did, handle: view.handle, diff --git a/packages/pds/src/app-view/services/actor/views.ts b/packages/pds/src/app-view/services/actor/views.ts index b0f652ef901..a00915b8d17 100644 --- a/packages/pds/src/app-view/services/actor/views.ts +++ b/packages/pds/src/app-view/services/actor/views.ts @@ -10,6 +10,7 @@ import { ImageUriBuilder } from '../../../image/uri' import { LabelService } from '../label' import { GraphService } from '../graph' import { LabelCache } from '../../../label-cache' +import { notSoftDeletedClause } from '../../../db/util' export class ActorViews { constructor( @@ -26,27 +27,35 @@ export class ActorViews { profileDetailed( result: ActorResult, viewer: string, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise profileDetailed( result: ActorResult[], viewer: string, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise async profileDetailed( result: ActorResult | ActorResult[], viewer: string, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise { const results = Array.isArray(result) ? result : [result] if (results.length === 0) return [] const { ref } = this.db.db.dynamic + const { skipLabels = false, includeSoftDeleted = false } = opts ?? {} const dids = results.map((r) => r.did) const profileInfosQb = this.db.db .selectFrom('did_handle') .where('did_handle.did', 'in', dids) + .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') .leftJoin('profile', 'profile.creator', 'did_handle.did') .leftJoin('profile_agg', 'profile_agg.did', 'did_handle.did') + .if(!includeSoftDeleted, (qb) => + qb.where(notSoftDeletedClause(ref('repo_root'))), + ) .select([ 'did_handle.did as did', 'profile.uri as profileUri', @@ -100,7 +109,7 @@ export class ActorViews { const [profileInfos, labels] = await Promise.all([ profileInfosQb.execute(), - this.services.label.getLabelsForSubjects(dids), + this.services.label.getLabelsForSubjects(skipLabels ? [] : dids), ]) const profileInfoByDid = profileInfos.reduce((acc, info) => { @@ -152,22 +161,36 @@ export class ActorViews { return Array.isArray(result) ? views : views[0] } - profile(result: ActorResult, viewer: string): Promise - profile(result: ActorResult[], viewer: string): Promise + profile( + result: ActorResult, + viewer: string, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, + ): Promise + profile( + result: ActorResult[], + viewer: string, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, + ): Promise async profile( result: ActorResult | ActorResult[], viewer: string, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise { const results = Array.isArray(result) ? result : [result] if (results.length === 0) return [] const { ref } = this.db.db.dynamic + const { skipLabels = false, includeSoftDeleted = false } = opts ?? {} const dids = results.map((r) => r.did) const profileInfosQb = this.db.db .selectFrom('did_handle') .where('did_handle.did', 'in', dids) + .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') .leftJoin('profile', 'profile.creator', 'did_handle.did') + .if(!includeSoftDeleted, (qb) => + qb.where(notSoftDeletedClause(ref('repo_root'))), + ) .select([ 'did_handle.did as did', 'profile.uri as profileUri', @@ -217,7 +240,7 @@ export class ActorViews { const [profileInfos, labels] = await Promise.all([ profileInfosQb.execute(), - this.services.label.getLabelsForSubjects(dids), + this.services.label.getLabelsForSubjects(skipLabels ? [] : dids), ]) const profileInfoByDid = profileInfos.reduce((acc, info) => { @@ -263,19 +286,25 @@ export class ActorViews { } // @NOTE keep in sync with feedService.getActorViews() - profileBasic(result: ActorResult, viewer: string): Promise + profileBasic( + result: ActorResult, + viewer: string, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, + ): Promise profileBasic( result: ActorResult[], viewer: string, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise async profileBasic( result: ActorResult | ActorResult[], viewer: string, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise { const results = Array.isArray(result) ? result : [result] if (results.length === 0) return [] - const profiles = await this.profile(results, viewer) + const profiles = await this.profile(results, viewer, opts) const views = profiles.map((view) => ({ did: view.did, handle: view.handle, From d960aa66dfebcf0f639db7c6e3dedfd0fbd09f0c Mon Sep 17 00:00:00 2001 From: Emily Liu Date: Mon, 31 Jul 2023 21:04:57 -0700 Subject: [PATCH 075/237] Update readme --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d4a9fa55f0f..12c6e58b880 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # AT Protocol (Authenticated Transfer Protocol) -This is a working repository for the "AT Protocol," aka the Authenticated Transfer Protocol. +This is a working repository for the AT Protocol, aka the Authenticated Transfer Protocol. --- @@ -10,7 +10,7 @@ This is a working repository for the "AT Protocol," aka the Authenticated Transf ## ℹ️ About this project -To learn more about ATP, see: +To learn more about atproto, see: - [Protocol Documentation](https://atproto.com/docs) - [Overview Guide](https://atproto.com/guides/overview) 👈 Good place to start @@ -31,7 +31,7 @@ While we do accept contributions, we prioritize high-quality issues and pull req - Check for existing issues before filing a new one, please. - Open an issue and give some time for discussion before submitting a PR. - If submitting a PR that includes a lexicon change, please get sign off on the lexicon change _before_ doing the implementation. -- Issues are for bugs & feature requests related to the Typescript implementation of atproto and related services. For high-level discussions, please you the [Discussion Forum](https://github.com/bluesky-social/atproto/discussions). For client issues, please use the relevant [social-app](https://github.com/bluesky-social/social-app) repo +- Issues are for bugs & feature requests related to the TypeScript implementation of atproto and related services. For high-level discussions, please you the [Discussion Forum](https://github.com/bluesky-social/atproto/discussions). For client issues, please use the relevant [social-app](https://github.com/bluesky-social/social-app) repo - Stay away from PRs that: - Refactor large parts of the codebase - Add entirely new features without prior discussion @@ -40,6 +40,12 @@ While we do accept contributions, we prioritize high-quality issues and pull req Remember, we serve a wide community of users. Our day-to-day involves us constantly asking "which top priority is our top priority." If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth. +## Are you a developer interested in building on atproto? + +Bluesky is an open social network built on the AT Protocol, a flexible technology that will never lock developers out of the ecosystems that they help build. With atproto, third-party can be as seamless as first-party through custom feeds, federated services, clients, and more. + +If you're a developer interested in building on atproto, we'd love to email you a Bluesky invite code. Simply share your GitHub (or similar) profile with us via [this form](https://forms.gle/BF21oxVNZiDjDhXF9). + ## Security disclosures If you discover any security issues, please send an email to security@bsky.app. The email is automatically CCed to the entire team, and we'll respond promptly. See [SECURITY.md](https://github.com/bluesky-social/atproto/blob/main/SECURITY.md) for more info. From 3befcfe35e214c99c80411a9631f0be014a451a1 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Mon, 31 Jul 2023 21:53:45 -0700 Subject: [PATCH 076/237] pds: set default postgresql URL to localhost (#1415) --- packages/pds/service/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/service/index.js b/packages/pds/service/index.js index bf42eac99e3..1341b38066a 100644 --- a/packages/pds/service/index.js +++ b/packages/pds/service/index.js @@ -97,7 +97,7 @@ const main = async () => { const pgUrl = ({ username = 'postgres', password = 'postgres', - host = '0.0.0.0', + host = 'localhost', port = '5432', database = 'postgres', sslmode, From 1ad62742025924c7e99af2abe42b013db7e5feff Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 1 Aug 2023 12:38:14 -0500 Subject: [PATCH 077/237] Actor service fix (#1414) * fix up bug in actor service * add in hydrate methods * build fix * couple more fixes * tidy * port to appview * dont build branch * fix issue with items in list * tidy * dont build branch * quick fix --- .../bsky/src/api/app/bsky/actor/getProfile.ts | 9 +- .../src/api/app/bsky/actor/getProfiles.ts | 2 +- .../src/api/app/bsky/actor/getSuggestions.ts | 5 +- .../src/api/app/bsky/actor/searchActors.ts | 4 +- .../app/bsky/actor/searchActorsTypeahead.ts | 2 +- .../src/api/app/bsky/feed/getActorFeeds.ts | 3 + .../bsky/src/api/app/bsky/feed/getLikes.ts | 21 +- .../src/api/app/bsky/feed/getRepostedBy.ts | 2 +- .../bsky/src/api/app/bsky/graph/getBlocks.ts | 5 +- .../src/api/app/bsky/graph/getFollowers.ts | 5 +- .../bsky/src/api/app/bsky/graph/getFollows.ts | 5 +- .../bsky/src/api/app/bsky/graph/getList.ts | 18 +- .../src/api/app/bsky/graph/getListMutes.ts | 12 +- .../bsky/src/api/app/bsky/graph/getLists.ts | 3 + .../bsky/src/api/app/bsky/graph/getMutes.ts | 2 +- .../bsky/notification/listNotifications.ts | 29 +- packages/bsky/src/services/actor/views.ts | 311 +++++++++--------- packages/bsky/src/services/feed/index.ts | 27 +- packages/common-web/src/arrays.ts | 13 + packages/common-web/src/index.ts | 1 + .../app-view/api/app/bsky/actor/getProfile.ts | 9 +- .../api/app/bsky/actor/getProfiles.ts | 2 +- .../api/app/bsky/actor/getSuggestions.ts | 3 +- .../api/app/bsky/actor/searchActors.ts | 2 +- .../app/bsky/actor/searchActorsTypeahead.ts | 2 +- .../api/app/bsky/feed/getActorFeeds.ts | 4 + .../app-view/api/app/bsky/feed/getLikes.ts | 19 +- .../api/app/bsky/feed/getRepostedBy.ts | 2 +- .../app-view/api/app/bsky/graph/getBlocks.ts | 5 +- .../api/app/bsky/graph/getFollowers.ts | 5 +- .../app-view/api/app/bsky/graph/getFollows.ts | 5 +- .../app-view/api/app/bsky/graph/getList.ts | 18 +- .../api/app/bsky/graph/getListMutes.ts | 12 +- .../app-view/api/app/bsky/graph/getLists.ts | 3 + .../app-view/api/app/bsky/graph/getMutes.ts | 2 +- .../bsky/notification/listNotifications.ts | 9 +- .../pds/src/app-view/services/actor/views.ts | 242 +++++++------- .../pds/src/app-view/services/feed/index.ts | 19 +- 38 files changed, 470 insertions(+), 372 deletions(-) create mode 100644 packages/common-web/src/arrays.ts diff --git a/packages/bsky/src/api/app/bsky/actor/getProfile.ts b/packages/bsky/src/api/app/bsky/actor/getProfile.ts index 51cbd313eaf..e1db6abfd1e 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfile.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfile.ts @@ -23,10 +23,17 @@ export default function (server: Server, ctx: AppContext) { 'AccountTakedown', ) } + const profile = await actorService.views.profileDetailed( + actorRes, + requester, + ) + if (!profile) { + throw new InvalidRequestError('Profile not found') + } return { encoding: 'application/json', - body: await actorService.views.profileDetailed(actorRes, requester), + body: profile, } }, }) diff --git a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts index ad676f01959..3b97a03475b 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts @@ -15,7 +15,7 @@ export default function (server: Server, ctx: AppContext) { return { encoding: 'application/json', body: { - profiles: await actorService.views.profileDetailed( + profiles: await actorService.views.hydrateProfilesDetailed( actorsRes, requester, ), diff --git a/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts b/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts index 37448cbf0d3..ba51957c36f 100644 --- a/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts +++ b/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts @@ -55,7 +55,10 @@ export default function (server: Server, ctx: AppContext) { encoding: 'application/json', body: { cursor: suggestionsRes.at(-1)?.did, - actors: await actorService.views.profile(suggestionsRes, viewer), + actors: await actorService.views.hydrateProfiles( + suggestionsRes, + viewer, + ), }, } }, diff --git a/packages/bsky/src/api/app/bsky/actor/searchActors.ts b/packages/bsky/src/api/app/bsky/actor/searchActors.ts index d29114432c5..9adc0b051db 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActors.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActors.ts @@ -24,7 +24,9 @@ export default function (server: Server, ctx: AppContext) { : [] const keyset = new SearchKeyset(sql``, sql``) - const actors = await services.actor(db).views.profile(results, requester) + const actors = await services + .actor(db) + .views.hydrateProfiles(results, requester) const filtered = actors.filter( (actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy, ) diff --git a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts index 9148634fa19..a20ecd1637f 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -22,7 +22,7 @@ export default function (server: Server, ctx: AppContext) { const actors = await services .actor(db) - .views.profileBasic(results, requester) + .views.hydrateProfilesBasic(results, requester) const filtered = actors.filter( (actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy, diff --git a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts index 5ca0a141585..a255bc944b0 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts @@ -37,6 +37,9 @@ export default function (server: Server, ctx: AppContext) { feedsQb.execute(), actorService.views.profile(creatorRes, viewer), ]) + if (!creatorProfile) { + throw new InvalidRequestError(`Actor not found: ${actor}`) + } const profiles = { [creatorProfile.did]: creatorProfile } const feeds = feedsRes.map((row) => { diff --git a/packages/bsky/src/api/app/bsky/feed/getLikes.ts b/packages/bsky/src/api/app/bsky/feed/getLikes.ts index 4476b3bbd2c..7a0d0551243 100644 --- a/packages/bsky/src/api/app/bsky/feed/getLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getLikes.ts @@ -1,3 +1,4 @@ +import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' import { paginate, TimeCidKeyset } from '../../../../db/pagination' import AppContext from '../../../../context' @@ -37,7 +38,19 @@ export default function (server: Server, ctx: AppContext) { }) const likesRes = await builder.execute() - const actors = await services.actor(db).views.profile(likesRes, requester) + const actors = await services + .actor(db) + .views.profiles(likesRes, requester) + + const likes = mapDefined(likesRes, (row) => + actors[row.did] + ? { + createdAt: row.createdAt, + indexedAt: row.indexedAt, + actor: actors[row.did], + } + : undefined, + ) return { encoding: 'application/json', @@ -45,11 +58,7 @@ export default function (server: Server, ctx: AppContext) { uri, cid, cursor: keyset.packFromResult(likesRes), - likes: likesRes.map((row, i) => ({ - createdAt: row.createdAt, - indexedAt: row.indexedAt, - actor: actors[i], - })), + likes, }, } }, diff --git a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts index c76e19b32b7..ff635641163 100644 --- a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts @@ -34,7 +34,7 @@ export default function (server: Server, ctx: AppContext) { const repostedByRes = await builder.execute() const repostedBy = await services .actor(db) - .views.profile(repostedByRes, requester) + .views.hydrateProfiles(repostedByRes, requester) return { encoding: 'application/json', diff --git a/packages/bsky/src/api/app/bsky/graph/getBlocks.ts b/packages/bsky/src/api/app/bsky/graph/getBlocks.ts index e2142debfa5..45af81a7ce3 100644 --- a/packages/bsky/src/api/app/bsky/graph/getBlocks.ts +++ b/packages/bsky/src/api/app/bsky/graph/getBlocks.ts @@ -33,7 +33,10 @@ export default function (server: Server, ctx: AppContext) { const blocksRes = await blocksReq.execute() const actorService = services.actor(db) - const blocks = await actorService.views.profile(blocksRes, requester) + const blocks = await actorService.views.hydrateProfiles( + blocksRes, + requester, + ) return { encoding: 'application/json', diff --git a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts index 6908d989af0..85f1dd78fbf 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts @@ -37,9 +37,12 @@ export default function (server: Server, ctx: AppContext) { const followersRes = await followersReq.execute() const [followers, subject] = await Promise.all([ - actorService.views.profile(followersRes, requester), + actorService.views.hydrateProfiles(followersRes, requester), actorService.views.profile(subjectRes, requester), ]) + if (!subject) { + throw new InvalidRequestError(`Actor not found: ${actor}`) + } return { encoding: 'application/json', diff --git a/packages/bsky/src/api/app/bsky/graph/getFollows.ts b/packages/bsky/src/api/app/bsky/graph/getFollows.ts index 2db016da037..3fdb45f2e47 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollows.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollows.ts @@ -37,9 +37,12 @@ export default function (server: Server, ctx: AppContext) { const followsRes = await followsReq.execute() const [follows, subject] = await Promise.all([ - actorService.views.profile(followsRes, requester), + actorService.views.hydrateProfiles(followsRes, requester), actorService.views.profile(creatorRes, requester), ]) + if (!subject) { + throw new InvalidRequestError(`Actor not found: ${actor}`) + } return { encoding: 'application/json', diff --git a/packages/bsky/src/api/app/bsky/graph/getList.ts b/packages/bsky/src/api/app/bsky/graph/getList.ts index 2814cf99e4e..1a12baa52ca 100644 --- a/packages/bsky/src/api/app/bsky/graph/getList.ts +++ b/packages/bsky/src/api/app/bsky/graph/getList.ts @@ -2,7 +2,6 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import { paginate, TimeCidKeyset } from '../../../../db/pagination' import AppContext from '../../../../context' -import { ProfileView } from '../../../../lexicon/types/app/bsky/actor/defs' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getList({ @@ -40,20 +39,17 @@ export default function (server: Server, ctx: AppContext) { const itemsRes = await itemsReq.execute() const actorService = services.actor(db) - const profiles = await actorService.views.profile(itemsRes, requester) - const profilesMap = profiles.reduce( - (acc, cur) => ({ - ...acc, - [cur.did]: cur, - }), - {} as Record, + const profiles = await actorService.views.hydrateProfiles( + itemsRes, + requester, ) - const items = itemsRes.map((item) => ({ - subject: profilesMap[item.did], - })) + const items = profiles.map((subject) => ({ subject })) const creator = await actorService.views.profile(listRes, requester) + if (!creator) { + throw new InvalidRequestError(`Actor not found: ${listRes.handle}`) + } const subject = { uri: listRes.uri, diff --git a/packages/bsky/src/api/app/bsky/graph/getListMutes.ts b/packages/bsky/src/api/app/bsky/graph/getListMutes.ts index f8f353bcaf6..d85b86f9189 100644 --- a/packages/bsky/src/api/app/bsky/graph/getListMutes.ts +++ b/packages/bsky/src/api/app/bsky/graph/getListMutes.ts @@ -1,7 +1,6 @@ import { Server } from '../../../../lexicon' import { paginate, TimeCidKeyset } from '../../../../db/pagination' import AppContext from '../../../../context' -import { ProfileView } from '../../../../lexicon/types/app/bsky/actor/defs' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getListMutes({ @@ -33,17 +32,10 @@ export default function (server: Server, ctx: AppContext) { const listsRes = await listsReq.execute() const actorService = ctx.services.actor(ctx.db) - const profiles = await actorService.views.profile(listsRes, requester) - const profilesMap = profiles.reduce( - (acc, cur) => ({ - ...acc, - [cur.did]: cur, - }), - {} as Record, - ) + const profiles = await actorService.views.profiles(listsRes, requester) const lists = listsRes.map((row) => - graphService.formatListView(row, profilesMap), + graphService.formatListView(row, profiles), ) return { diff --git a/packages/bsky/src/api/app/bsky/graph/getLists.ts b/packages/bsky/src/api/app/bsky/graph/getLists.ts index b325a8a435b..2f2d7878623 100644 --- a/packages/bsky/src/api/app/bsky/graph/getLists.ts +++ b/packages/bsky/src/api/app/bsky/graph/getLists.ts @@ -35,6 +35,9 @@ export default function (server: Server, ctx: AppContext) { listsReq.execute(), actorService.views.profile(creatorRes, requester), ]) + if (!creator) { + throw new InvalidRequestError(`Actor not found: ${actor}`) + } const profileMap = { [creator.did]: creator, } diff --git a/packages/bsky/src/api/app/bsky/graph/getMutes.ts b/packages/bsky/src/api/app/bsky/graph/getMutes.ts index fa8b53f09d4..0725089616a 100644 --- a/packages/bsky/src/api/app/bsky/graph/getMutes.ts +++ b/packages/bsky/src/api/app/bsky/graph/getMutes.ts @@ -38,7 +38,7 @@ export default function (server: Server, ctx: AppContext) { encoding: 'application/json', body: { cursor: keyset.packFromResult(mutesRes), - mutes: await actorService.views.profile(mutesRes, requester), + mutes: await actorService.views.hydrateProfiles(mutesRes, requester), }, } }, diff --git a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts index 9aec05ed8aa..463b9a6f293 100644 --- a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts @@ -1,5 +1,6 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { jsonStringToLex } from '@atproto/lexicon' +import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' import { paginate, TimeCidKeyset } from '../../../../db/pagination' import AppContext from '../../../../context' @@ -78,7 +79,7 @@ export default function (server: Server, ctx: AppContext) { const labelService = ctx.services.label(ctx.db) const recordUris = notifs.map((notif) => notif.uri) const [authors, labels] = await Promise.all([ - actorService.views.profile( + actorService.views.profiles( notifs.map((notif) => ({ did: notif.authorDid, handle: notif.authorHandle, @@ -90,17 +91,21 @@ export default function (server: Server, ctx: AppContext) { labelService.getLabelsForUris(recordUris), ]) - const notifications = notifs.map((notif, i) => ({ - uri: notif.uri, - cid: notif.cid, - author: authors[i], - reason: notif.reason, - reasonSubject: notif.reasonSubject || undefined, - record: jsonStringToLex(notif.recordJson) as Record, - isRead: seenAt ? notif.indexedAt <= seenAt : false, - indexedAt: notif.indexedAt, - labels: labels[notif.uri] ?? [], - })) + const notifications = mapDefined(notifs, (notif) => { + const author = authors[notif.authorDid] + if (!author) return undefined + return { + uri: notif.uri, + cid: notif.cid, + author, + reason: notif.reason, + reasonSubject: notif.reasonSubject || undefined, + record: jsonStringToLex(notif.recordJson) as Record, + isRead: seenAt ? notif.indexedAt <= seenAt : false, + indexedAt: notif.indexedAt, + labels: labels[notif.uri] ?? [], + } + }) return { encoding: 'application/json', diff --git a/packages/bsky/src/services/actor/views.ts b/packages/bsky/src/services/actor/views.ts index 4bf75bef500..7f0cc86ef03 100644 --- a/packages/bsky/src/services/actor/views.ts +++ b/packages/bsky/src/services/actor/views.ts @@ -1,4 +1,4 @@ -import { ArrayEl } from '@atproto/common' +import { mapDefined } from '@atproto/common' import { INVALID_HANDLE } from '@atproto/identifier' import { ProfileViewDetailed, @@ -10,32 +10,22 @@ import { noMatch, notSoftDeletedClause } from '../../db/util' import { Actor } from '../../db/tables/actor' import { ImageUriBuilder } from '../../image/uri' import { LabelService } from '../label' -import { ListViewBasic } from '../../lexicon/types/app/bsky/graph/defs' +import { GraphService } from '../graph' export class ActorViews { constructor(private db: Database, private imgUriBuilder: ImageUriBuilder) {} services = { - label: LabelService.creator(), + label: LabelService.creator()(this.db), + graph: GraphService.creator(this.imgUriBuilder)(this.db), } - profileDetailed( - result: ActorResult, - viewer: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise - profileDetailed( - result: ActorResult[], - viewer: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise - async profileDetailed( - result: ActorResult | ActorResult[], + async profilesDetailed( + results: ActorResult[], viewer: string | null, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const results = Array.isArray(result) ? result : [result] - if (results.length === 0) return [] + ): Promise> { + if (results.length === 0) return {} const { ref } = this.db.db.dynamic const { skipLabels = false, includeSoftDeleted = false } = opts ?? {} @@ -51,6 +41,7 @@ export class ActorViews { ) .select([ 'actor.did as did', + 'actor.handle as handle', 'profile.uri as profileUri', 'profile.displayName as displayName', 'profile.description as description', @@ -95,79 +86,101 @@ export class ActorViews { .where('mutedByDid', '=', viewer ?? '') .select('subjectDid') .as('requesterMuted'), + this.db.db + .selectFrom('list_item') + .if(!viewer, (q) => q.where(noMatch)) + .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') + .where('list_mute.mutedByDid', '=', viewer ?? '') + .whereRef('list_item.subjectDid', '=', ref('actor.did')) + .select('list_item.listUri') + .limit(1) + .as('requesterMutedByList'), ]) - const [profileInfos, labels, listMutes] = await Promise.all([ + const [profileInfos, labels] = await Promise.all([ profileInfosQb.execute(), - this.services.label(this.db).getLabelsForSubjects(skipLabels ? [] : dids), - this.getListMutes(dids, viewer), + this.services.label.getLabelsForSubjects(skipLabels ? [] : dids), ]) - const profileInfoByDid = profileInfos.reduce((acc, info) => { - return Object.assign(acc, { [info.did]: info }) - }, {} as Record>) + const listUris: string[] = profileInfos + .map((a) => a.requesterMutedByList) + .filter((list) => !!list) + const listViews = await this.services.graph.getListViews(listUris, viewer) - const views = results.map((result) => { - const profileInfo = profileInfoByDid[result.did] - const avatar = profileInfo?.avatarCid + return profileInfos.reduce((acc, cur) => { + const actorLabels = labels[cur.did] ?? [] + const avatar = cur?.avatarCid ? this.imgUriBuilder.getCommonSignedUri( 'avatar', - profileInfo.did, - profileInfo.avatarCid, + cur.did, + cur.avatarCid, ) : undefined - const banner = profileInfo?.bannerCid + const banner = cur?.bannerCid ? this.imgUriBuilder.getCommonSignedUri( 'banner', - profileInfo.did, - profileInfo.bannerCid, + cur.did, + cur.bannerCid, ) : undefined - return { - did: result.did, - handle: result.handle ?? INVALID_HANDLE, - displayName: profileInfo?.displayName || undefined, - description: profileInfo?.description || undefined, + const mutedByList = + cur.requesterMutedByList && listViews[cur.requesterMutedByList] + ? this.services.graph.formatListViewBasic( + listViews[cur.requesterMutedByList], + ) + : undefined + const profile = { + did: cur.did, + handle: cur.handle ?? INVALID_HANDLE, + displayName: cur?.displayName || undefined, + description: cur?.description || undefined, avatar, banner, - followsCount: profileInfo?.followsCount ?? 0, - followersCount: profileInfo?.followersCount ?? 0, - postsCount: profileInfo?.postsCount ?? 0, - indexedAt: profileInfo?.indexedAt || undefined, + followsCount: cur?.followsCount ?? 0, + followersCount: cur?.followersCount ?? 0, + postsCount: cur?.postsCount ?? 0, + indexedAt: cur?.indexedAt || undefined, viewer: viewer ? { - following: profileInfo?.requesterFollowing || undefined, - followedBy: profileInfo?.requesterFollowedBy || undefined, - muted: !!profileInfo?.requesterMuted || !!listMutes[result.did], - mutedByList: listMutes[result.did], - blockedBy: !!profileInfo.requesterBlockedBy, - blocking: profileInfo.requesterBlocking || undefined, + following: cur?.requesterFollowing || undefined, + followedBy: cur?.requesterFollowedBy || undefined, + muted: !!cur?.requesterMuted || !!cur.requesterMutedByList, + mutedByList, + blockedBy: !!cur.requesterBlockedBy, + blocking: cur.requesterBlocking || undefined, } : undefined, - labels: labels[result.did] ?? [], + labels: skipLabels ? undefined : actorLabels, } - }) - - return Array.isArray(result) ? views : views[0] + acc[cur.did] = profile + return acc + }, {} as Record) } - profile( - result: ActorResult, + async hydrateProfilesDetailed( + results: ActorResult[], viewer: string | null, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise - profile( - result: ActorResult[], + ): Promise { + const profiles = await this.profilesDetailed(results, viewer, opts) + return mapDefined(results, (result) => profiles[result.did]) + } + + async profileDetailed( + result: ActorResult, viewer: string | null, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise - async profile( - result: ActorResult | ActorResult[], + ): Promise { + const profiles = await this.profilesDetailed([result], viewer, opts) + return profiles[result.did] ?? null + } + + async profiles( + results: ActorResult[], viewer: string | null, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const results = Array.isArray(result) ? result : [result] - if (results.length === 0) return [] + ): Promise> { + if (results.length === 0) return {} const { ref } = this.db.db.dynamic const { skipLabels = false, includeSoftDeleted = false } = opts ?? {} @@ -182,6 +195,7 @@ export class ActorViews { ) .select([ 'actor.did as did', + 'actor.handle as handle', 'profile.uri as profileUri', 'profile.displayName as displayName', 'profile.description as description', @@ -222,120 +236,121 @@ export class ActorViews { .where('mutedByDid', '=', viewer ?? '') .select('subjectDid') .as('requesterMuted'), + this.db.db + .selectFrom('list_item') + .if(!viewer, (q) => q.where(noMatch)) + .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') + .where('list_mute.mutedByDid', '=', viewer ?? '') + .whereRef('list_item.subjectDid', '=', ref('actor.did')) + .select('list_item.listUri') + .limit(1) + .as('requesterMutedByList'), ]) - const [profileInfos, labels, listMutes] = await Promise.all([ + const [profileInfos, labels] = await Promise.all([ profileInfosQb.execute(), - this.services.label(this.db).getLabelsForSubjects(skipLabels ? [] : dids), - this.getListMutes(dids, viewer), + this.services.label.getLabelsForSubjects(skipLabels ? [] : dids), ]) - const profileInfoByDid = profileInfos.reduce((acc, info) => { - return Object.assign(acc, { [info.did]: info }) - }, {} as Record>) + const listUris: string[] = profileInfos + .map((a) => a.requesterMutedByList) + .filter((list) => !!list) + const listViews = await this.services.graph.getListViews(listUris, viewer) - const views = results.map((result) => { - const profileInfo = profileInfoByDid[result.did] - const avatar = profileInfo?.avatarCid + return profileInfos.reduce((acc, cur) => { + const actorLabels = labels[cur.did] ?? [] + const avatar = cur?.avatarCid ? this.imgUriBuilder.getCommonSignedUri( 'avatar', - profileInfo.did, - profileInfo.avatarCid, + cur.did, + cur.avatarCid, ) : undefined - return { - did: result.did, - handle: result.handle ?? INVALID_HANDLE, - displayName: profileInfo?.displayName || undefined, - description: profileInfo?.description || undefined, + const mutedByList = + cur.requesterMutedByList && listViews[cur.requesterMutedByList] + ? this.services.graph.formatListViewBasic( + listViews[cur.requesterMutedByList], + ) + : undefined + const profile = { + did: cur.did, + handle: cur.handle ?? INVALID_HANDLE, + displayName: cur?.displayName || undefined, + description: cur?.description || undefined, avatar, - indexedAt: profileInfo?.indexedAt || undefined, + indexedAt: cur?.indexedAt || undefined, viewer: viewer ? { - muted: !!profileInfo?.requesterMuted || !!listMutes[result.did], - mutedByList: listMutes[result.did], - blockedBy: !!profileInfo.requesterBlockedBy, - blocking: profileInfo.requesterBlocking || undefined, - following: profileInfo?.requesterFollowing || undefined, - followedBy: profileInfo?.requesterFollowedBy || undefined, + muted: !!cur?.requesterMuted || !!cur.requesterMutedByList, + mutedByList, + blockedBy: !!cur.requesterBlockedBy, + blocking: cur.requesterBlocking || undefined, + following: cur?.requesterFollowing || undefined, + followedBy: cur?.requesterFollowedBy || undefined, } : undefined, - labels: labels[result.did] ?? [], + labels: skipLabels ? undefined : actorLabels, } - }) + acc[cur.did] = profile + return acc + }, {} as Record) + } - return Array.isArray(result) ? views : views[0] + async hydrateProfiles( + results: ActorResult[], + viewer: string | null, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, + ): Promise { + const profiles = await this.profiles(results, viewer, opts) + return mapDefined(results, (result) => profiles[result.did]) } - // @NOTE keep in sync with feedService.getActorViews() - profileBasic( + async profile( result: ActorResult, viewer: string | null, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise - profileBasic( - result: ActorResult[], + ): Promise { + const profiles = await this.profiles([result], viewer, opts) + return profiles[result.did] ?? null + } + + // @NOTE keep in sync with feedService.getActorViews() + async profilesBasic( + results: ActorResult[], viewer: string | null, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise - async profileBasic( - result: ActorResult | ActorResult[], + ): Promise> { + if (results.length === 0) return {} + const profiles = await this.profiles(results, viewer, opts) + return Object.values(profiles).reduce((acc, cur) => { + const profile = { + did: cur.did, + handle: cur.handle, + displayName: cur.displayName, + avatar: cur.avatar, + viewer: cur.viewer, + } + acc[cur.did] = profile + return acc + }, {} as Record) + } + + async hydrateProfilesBasic( + results: ActorResult[], viewer: string | null, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const results = Array.isArray(result) ? result : [result] - if (results.length === 0) return [] - - const profiles = await this.profile(results, viewer, opts) - const views = profiles.map((view) => ({ - did: view.did, - handle: view.handle, - displayName: view.displayName, - avatar: view.avatar, - viewer: view.viewer, - })) - - return Array.isArray(result) ? views : views[0] + ): Promise { + const profiles = await this.profilesBasic(results, viewer, opts) + return mapDefined(results, (result) => profiles[result.did]) } - async getListMutes( - subjects: string[], - mutedBy: string | null, - ): Promise> { - if (mutedBy === null) return {} - if (subjects.length < 1) return {} - const res = await this.db.db - .selectFrom('list_item') - .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') - .innerJoin('list', 'list.uri', 'list_item.listUri') - .where('list_mute.mutedByDid', '=', mutedBy) - .where('list_item.subjectDid', 'in', subjects) - .selectAll('list') - .select('list_item.subjectDid as subjectDid') - .execute() - return res.reduce( - (acc, cur) => ({ - ...acc, - [cur.subjectDid]: { - uri: cur.uri, - cid: cur.cid, - name: cur.name, - purpose: cur.purpose, - avatar: cur.avatarCid - ? this.imgUriBuilder.getCommonSignedUri( - 'avatar', - cur.creator, - cur.avatarCid, - ) - : undefined, - viewer: { - muted: true, - }, - indexedAt: cur.indexedAt, - }, - }), - {} as Record, - ) + async profileBasic( + result: ActorResult, + viewer: string | null, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, + ): Promise { + const profiles = await this.profilesBasic([result], viewer, opts) + return profiles[result.did] ?? null } } diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index c2156d2ca33..10d42731ea7 100644 --- a/packages/bsky/src/services/feed/index.ts +++ b/packages/bsky/src/services/feed/index.ts @@ -191,27 +191,30 @@ export class FeedService { const listViews = await this.services.graph.getListViews(listUris, viewer) return actors.reduce((acc, cur) => { const actorLabels = labels[cur.did] ?? [] + const avatar = cur.avatarCid + ? this.imgUriBuilder.getCommonSignedUri( + 'avatar', + cur.did, + cur.avatarCid, + ) + : undefined + const mutedByList = + cur.requesterMutedByList && listViews[cur.requesterMutedByList] + ? this.services.graph.formatListViewBasic( + listViews[cur.requesterMutedByList], + ) + : undefined return { ...acc, [cur.did]: { did: cur.did, handle: cur.handle ?? INVALID_HANDLE, displayName: cur.displayName ?? undefined, - avatar: cur.avatarCid - ? this.imgUriBuilder.getCommonSignedUri( - 'avatar', - cur.did, - cur.avatarCid, - ) - : undefined, + avatar, viewer: viewer ? { muted: !!cur?.requesterMuted || !!cur?.requesterMutedByList, - mutedByList: cur.requesterMutedByList - ? this.services.graph.formatListViewBasic( - listViews[cur.requesterMutedByList], - ) - : undefined, + mutedByList, blockedBy: !!cur?.requesterBlockedBy, blocking: cur?.requesterBlocking || undefined, following: cur?.requesterFollowing || undefined, diff --git a/packages/common-web/src/arrays.ts b/packages/common-web/src/arrays.ts new file mode 100644 index 00000000000..51598fc86f1 --- /dev/null +++ b/packages/common-web/src/arrays.ts @@ -0,0 +1,13 @@ +export const mapDefined = ( + arr: T[], + fn: (obj: T) => S | undefined, +): S[] => { + const output: S[] = [] + for (const item of arr) { + const val = fn(item) + if (val !== undefined) { + output.push(val) + } + } + return output +} diff --git a/packages/common-web/src/index.ts b/packages/common-web/src/index.ts index 8be988e7035..e125677496f 100644 --- a/packages/common-web/src/index.ts +++ b/packages/common-web/src/index.ts @@ -1,6 +1,7 @@ export * as check from './check' export * as util from './util' +export * from './arrays' export * from './async' export * from './util' export * from './tid' diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts b/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts index df37174f7e5..f84ecb7ef9d 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts @@ -34,10 +34,17 @@ export default function (server: Server, ctx: AppContext) { 'AccountTakedown', ) } + const profile = await actorService.views.profileDetailed( + actorRes, + requester, + ) + if (!profile) { + throw new InvalidRequestError('Profile not found') + } return { encoding: 'application/json', - body: await actorService.views.profileDetailed(actorRes, requester), + body: profile, } }, }) diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts b/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts index 673868df384..78719975c30 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts @@ -26,7 +26,7 @@ export default function (server: Server, ctx: AppContext) { return { encoding: 'application/json', body: { - profiles: await actorService.views.profileDetailed( + profiles: await actorService.views.hydrateProfilesDetailed( actorsRes, requester, ), diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts b/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts index 540f7795122..edd426fe9e7 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts @@ -64,14 +64,13 @@ export default function (server: Server, ctx: AppContext) { } const suggestionsRes = await suggestionsQb.execute() - return { encoding: 'application/json', body: { cursor: suggestionsRes.at(-1)?.did, actors: await services.appView .actor(ctx.db) - .views.profile(suggestionsRes, requester), + .views.hydrateProfiles(suggestionsRes, requester), }, } }, diff --git a/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts b/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts index 380fea56ae4..20516204254 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts @@ -52,7 +52,7 @@ export default function (server: Server, ctx: AppContext) { const actors = await services.appView .actor(db) - .views.profile(results, requester) + .views.hydrateProfiles(results, requester) const filtered = actors.filter( (actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy, diff --git a/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts index 2cf879af954..9a133c59ccb 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts @@ -48,7 +48,7 @@ export default function (server: Server, ctx: AppContext) { const actors = await services.appView .actor(db) - .views.profileBasic(results, requester) + .views.hydrateProfilesBasic(results, requester) const filtered = actors.filter( (actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy, diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts b/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts index 6cc8becbeac..4ddbe49188c 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts @@ -48,6 +48,10 @@ export default function (server: Server, ctx: AppContext) { feedsQb.execute(), actorService.views.profile(creatorRes, requester), ]) + if (!creatorProfile) { + throw new InvalidRequestError(`Actor not found: ${actor}`) + } + const profiles = { [creatorProfile.did]: creatorProfile } const feeds = feedsRes.map((row) => diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts b/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts index 262dd678f39..63d82d4d53d 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts @@ -1,3 +1,4 @@ +import { mapDefined } from '@atproto/common' import { Server } from '../../../../../lexicon' import { paginate, TimeCidKeyset } from '../../../../../db/pagination' import AppContext from '../../../../../context' @@ -57,7 +58,17 @@ export default function (server: Server, ctx: AppContext) { const likesRes = await builder.execute() const actors = await services.appView .actor(db) - .views.profile(likesRes, requester) + .views.profiles(likesRes, requester) + + const likes = mapDefined(likesRes, (row) => + actors[row.did] + ? { + createdAt: row.createdAt, + indexedAt: row.indexedAt, + actor: actors[row.did], + } + : undefined, + ) return { encoding: 'application/json', @@ -65,11 +76,7 @@ export default function (server: Server, ctx: AppContext) { uri, cid, cursor: keyset.packFromResult(likesRes), - likes: likesRes.map((row, i) => ({ - createdAt: row.createdAt, - indexedAt: row.indexedAt, - actor: actors[i], - })), + likes, }, } }, diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts b/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts index 4632883d0ef..fb243809a79 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts @@ -58,7 +58,7 @@ export default function (server: Server, ctx: AppContext) { const repostedByRes = await builder.execute() const repostedBy = await services.appView .actor(db) - .views.profile(repostedByRes, requester) + .views.hydrateProfiles(repostedByRes, requester) return { encoding: 'application/json', diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts b/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts index e76eb2963a1..b3eaef40af5 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts @@ -56,7 +56,10 @@ export default function (server: Server, ctx: AppContext) { const blocksRes = await blocksReq.execute() const actorService = services.appView.actor(db) - const blocks = await actorService.views.profile(blocksRes, requester) + const blocks = await actorService.views.hydrateProfiles( + blocksRes, + requester, + ) return { encoding: 'application/json', diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts index 93bf73075e0..d74a4c26f0c 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts @@ -60,9 +60,12 @@ export default function (server: Server, ctx: AppContext) { const followersRes = await followersReq.execute() const [followers, subject] = await Promise.all([ - actorService.views.profile(followersRes, requester), + actorService.views.hydrateProfiles(followersRes, requester), actorService.views.profile(subjectRes, requester), ]) + if (!subject) { + throw new InvalidRequestError(`Actor not found: ${actor}`) + } return { encoding: 'application/json', diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts b/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts index 75edf58c225..703b83556ac 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts @@ -60,9 +60,12 @@ export default function (server: Server, ctx: AppContext) { const followsRes = await followsReq.execute() const [follows, subject] = await Promise.all([ - actorService.views.profile(followsRes, requester), + actorService.views.hydrateProfiles(followsRes, requester), actorService.views.profile(creatorRes, requester), ]) + if (!subject) { + throw new InvalidRequestError(`Actor not found: ${actor}`) + } return { encoding: 'application/json', diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getList.ts b/packages/pds/src/app-view/api/app/bsky/graph/getList.ts index c711ea93026..00eeaba88a5 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getList.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getList.ts @@ -2,7 +2,6 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../../lexicon' import { paginate, TimeCidKeyset } from '../../../../../db/pagination' import AppContext from '../../../../../context' -import { ProfileView } from '../../../../../lexicon/types/app/bsky/actor/defs' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getList({ @@ -51,20 +50,17 @@ export default function (server: Server, ctx: AppContext) { const itemsRes = await itemsReq.execute() const actorService = services.appView.actor(db) - const profiles = await actorService.views.profile(itemsRes, requester) - const profilesMap = profiles.reduce( - (acc, cur) => ({ - ...acc, - [cur.did]: cur, - }), - {} as Record, + const profiles = await actorService.views.hydrateProfiles( + itemsRes, + requester, ) - const items = itemsRes.map((item) => ({ - subject: profilesMap[item.did], - })) + const items = profiles.map((subject) => ({ subject })) const creator = await actorService.views.profile(listRes, requester) + if (!creator) { + throw new InvalidRequestError(`Actor not found: ${listRes.handle}`) + } const subject = { uri: listRes.uri, diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts b/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts index 89501548f40..21a64437ae0 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts @@ -1,7 +1,6 @@ import { Server } from '../../../../../lexicon' import { paginate, TimeCidKeyset } from '../../../../../db/pagination' import AppContext from '../../../../../context' -import { ProfileView } from '../../../../../lexicon/types/app/bsky/actor/defs' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getListMutes({ @@ -44,17 +43,10 @@ export default function (server: Server, ctx: AppContext) { const listsRes = await listsReq.execute() const actorService = ctx.services.appView.actor(ctx.db) - const profiles = await actorService.views.profile(listsRes, requester) - const profilesMap = profiles.reduce( - (acc, cur) => ({ - ...acc, - [cur.did]: cur, - }), - {} as Record, - ) + const profiles = await actorService.views.profiles(listsRes, requester) const lists = listsRes.map((row) => - graphService.formatListView(row, profilesMap), + graphService.formatListView(row, profiles), ) return { diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts b/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts index 7dc77e6c5ef..58b83a20db2 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts @@ -46,6 +46,9 @@ export default function (server: Server, ctx: AppContext) { listsReq.execute(), actorService.views.profile(creatorRes, requester), ]) + if (!creator) { + throw new InvalidRequestError(`Actor not found: ${actor}`) + } const profileMap = { [creator.did]: creator, } diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts b/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts index 51950515cd4..c26b7b1db25 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts @@ -51,7 +51,7 @@ export default function (server: Server, ctx: AppContext) { encoding: 'application/json', body: { cursor: keyset.packFromResult(mutesRes), - mutes: await actorService.views.profile(mutesRes, requester), + mutes: await actorService.views.hydrateProfiles(mutesRes, requester), }, } }, diff --git a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts b/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts index 2967c069ad9..e97f54992ba 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts +++ b/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts @@ -112,7 +112,7 @@ export default function (server: Server, ctx: AppContext) { const recordUris = notifs.map((notif) => notif.uri) const [blocks, authors, labels] = await Promise.all([ blocksQb ? blocksQb.execute() : emptyBlocksResult, - actorService.views.profile( + actorService.views.profiles( notifs.map((notif) => ({ did: notif.authorDid, handle: notif.authorHandle, @@ -127,13 +127,14 @@ export default function (server: Server, ctx: AppContext) { return acc }, {} as Record) - const notifications = notifs.flatMap((notif, i) => { + const notifications = common.mapDefined(notifs, (notif) => { const bytes = bytesByCid[notif.cid] - if (!bytes) return [] // Filter out + const author = authors[notif.authorDid] + if (!bytes || !author) return undefined return { uri: notif.uri, cid: notif.cid, - author: authors[i], + author: authors[notif.authorDid], reason: notif.reason, reasonSubject: notif.reasonSubject || undefined, record: common.cborBytesToRecord(bytes), diff --git a/packages/pds/src/app-view/services/actor/views.ts b/packages/pds/src/app-view/services/actor/views.ts index a00915b8d17..6f0e6a8d23b 100644 --- a/packages/pds/src/app-view/services/actor/views.ts +++ b/packages/pds/src/app-view/services/actor/views.ts @@ -1,4 +1,4 @@ -import { ArrayEl } from '@atproto/common' +import { mapDefined } from '@atproto/common' import { ProfileViewDetailed, ProfileView, @@ -24,23 +24,12 @@ export class ActorViews { graph: GraphService.creator(this.imgUriBuilder)(this.db), } - profileDetailed( - result: ActorResult, - viewer: string, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise - profileDetailed( - result: ActorResult[], + async profilesDetailed( + results: ActorResult[], viewer: string, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise - async profileDetailed( - result: ActorResult | ActorResult[], - viewer: string, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const results = Array.isArray(result) ? result : [result] - if (results.length === 0) return [] + ): Promise> { + if (results.length === 0) return {} const { ref } = this.db.db.dynamic const { skipLabels = false, includeSoftDeleted = false } = opts ?? {} @@ -58,6 +47,7 @@ export class ActorViews { ) .select([ 'did_handle.did as did', + 'did_handle.handle as handle', 'profile.uri as profileUri', 'profile.displayName as displayName', 'profile.description as description', @@ -112,72 +102,75 @@ export class ActorViews { this.services.label.getLabelsForSubjects(skipLabels ? [] : dids), ]) - const profileInfoByDid = profileInfos.reduce((acc, info) => { - return Object.assign(acc, { [info.did]: info }) - }, {} as Record>) - const listUris: string[] = profileInfos .map((a) => a.requesterMutedByList) .filter((list) => !!list) const listViews = await this.services.graph.getListViews(listUris, viewer) - const views = results.map((result) => { - const profileInfo = profileInfoByDid[result.did] - const avatar = profileInfo?.avatarCid - ? this.imgUriBuilder.getCommonSignedUri('avatar', profileInfo.avatarCid) + return profileInfos.reduce((acc, cur) => { + const actorLabels = labels[cur.did] ?? [] + const avatar = cur?.avatarCid + ? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid) : undefined - const banner = profileInfo?.bannerCid - ? this.imgUriBuilder.getCommonSignedUri('banner', profileInfo.bannerCid) + const banner = cur?.bannerCid + ? this.imgUriBuilder.getCommonSignedUri('banner', cur.bannerCid) : undefined - return { - did: result.did, - handle: result.handle, - displayName: truncateUtf8(profileInfo?.displayName, 64) || undefined, - description: truncateUtf8(profileInfo?.description, 256) || undefined, + const mutedByList = + cur.requesterMutedByList && listViews[cur.requesterMutedByList] + ? this.services.graph.formatListViewBasic( + listViews[cur.requesterMutedByList], + ) + : undefined + const profile = { + did: cur.did, + handle: cur.handle, + displayName: truncateUtf8(cur?.displayName, 64) || undefined, + description: truncateUtf8(cur?.description, 256) || undefined, avatar, banner, - followsCount: profileInfo?.followsCount || 0, - followersCount: profileInfo?.followersCount || 0, - postsCount: profileInfo?.postsCount || 0, - indexedAt: profileInfo?.indexedAt || undefined, + followsCount: cur?.followsCount || 0, + followersCount: cur?.followersCount || 0, + postsCount: cur?.postsCount || 0, + indexedAt: cur?.indexedAt || undefined, viewer: { - muted: - !!profileInfo?.requesterMuted || - !!profileInfo?.requesterMutedByList, - mutedByList: profileInfo.requesterMutedByList - ? this.services.graph.formatListViewBasic( - listViews[profileInfo.requesterMutedByList], - ) - : undefined, - blockedBy: !!profileInfo.requesterBlockedBy, - blocking: profileInfo.requesterBlocking || undefined, - following: profileInfo?.requesterFollowing || undefined, - followedBy: profileInfo?.requesterFollowedBy || undefined, + muted: !!cur?.requesterMuted || !!cur?.requesterMutedByList, + mutedByList, + blockedBy: !!cur.requesterBlockedBy, + blocking: cur.requesterBlocking || undefined, + following: cur?.requesterFollowing || undefined, + followedBy: cur?.requesterFollowedBy || undefined, }, - labels: labels[result.did] ?? [], + labels: skipLabels ? undefined : actorLabels, } - }) - - return Array.isArray(result) ? views : views[0] + acc[cur.did] = profile + return acc + }, {} as Record) } - profile( - result: ActorResult, + async hydrateProfilesDetailed( + results: ActorResult[], viewer: string, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise - profile( - result: ActorResult[], + ): Promise { + const profiles = await this.profilesDetailed(results, viewer, opts) + return mapDefined(results, (result) => profiles[result.did]) + } + + async profileDetailed( + result: ActorResult, viewer: string, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise - async profile( - result: ActorResult | ActorResult[], + ): Promise { + const profiles = await this.profilesDetailed([result], viewer, opts) + return profiles[result.did] ?? null + } + + async profiles( + results: ActorResult[], viewer: string, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const results = Array.isArray(result) ? result : [result] - if (results.length === 0) return [] + ): Promise> { + if (results.length === 0) return {} const { ref } = this.db.db.dynamic const { skipLabels = false, includeSoftDeleted = false } = opts ?? {} @@ -193,6 +186,7 @@ export class ActorViews { ) .select([ 'did_handle.did as did', + 'did_handle.handle as handle', 'profile.uri as profileUri', 'profile.displayName as displayName', 'profile.description as description', @@ -243,78 +237,100 @@ export class ActorViews { this.services.label.getLabelsForSubjects(skipLabels ? [] : dids), ]) - const profileInfoByDid = profileInfos.reduce((acc, info) => { - return Object.assign(acc, { [info.did]: info }) - }, {} as Record>) - const listUris: string[] = profileInfos .map((a) => a.requesterMutedByList) .filter((list) => !!list) const listViews = await this.services.graph.getListViews(listUris, viewer) - const views = results.map((result) => { - const profileInfo = profileInfoByDid[result.did] - const avatar = profileInfo?.avatarCid - ? this.imgUriBuilder.getCommonSignedUri('avatar', profileInfo.avatarCid) + return profileInfos.reduce((acc, cur) => { + const actorLabels = labels[cur.did] ?? [] + const avatar = cur.avatarCid + ? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid) : undefined - return { - did: result.did, - handle: result.handle, - displayName: truncateUtf8(profileInfo?.displayName, 64) || undefined, - description: truncateUtf8(profileInfo?.description, 256) || undefined, + const mutedByList = + cur.requesterMutedByList && listViews[cur.requesterMutedByList] + ? this.services.graph.formatListViewBasic( + listViews[cur.requesterMutedByList], + ) + : undefined + const profile = { + did: cur.did, + handle: cur.handle, + displayName: truncateUtf8(cur?.displayName, 64) || undefined, + description: truncateUtf8(cur?.description, 256) || undefined, avatar, - indexedAt: profileInfo?.indexedAt || undefined, + indexedAt: cur?.indexedAt || undefined, viewer: { - muted: - !!profileInfo?.requesterMuted || - !!profileInfo?.requesterMutedByList, - mutedByList: profileInfo.requesterMutedByList - ? this.services.graph.formatListViewBasic( - listViews[profileInfo.requesterMutedByList], - ) - : undefined, - blockedBy: !!profileInfo.requesterBlockedBy, - blocking: profileInfo.requesterBlocking || undefined, - following: profileInfo?.requesterFollowing || undefined, - followedBy: profileInfo?.requesterFollowedBy || undefined, + muted: !!cur?.requesterMuted || !!cur?.requesterMutedByList, + mutedByList, + blockedBy: !!cur.requesterBlockedBy, + blocking: cur.requesterBlocking || undefined, + following: cur?.requesterFollowing || undefined, + followedBy: cur?.requesterFollowedBy || undefined, }, - labels: labels[result.did] ?? [], + labels: skipLabels ? undefined : actorLabels, } - }) + acc[cur.did] = profile + return acc + }, {} as Record) + } - return Array.isArray(result) ? views : views[0] + async hydrateProfiles( + results: ActorResult[], + viewer: string, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, + ): Promise { + const profiles = await this.profiles(results, viewer, opts) + return mapDefined(results, (result) => profiles[result.did]) } - // @NOTE keep in sync with feedService.getActorViews() - profileBasic( + async profile( result: ActorResult, viewer: string, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise - profileBasic( - result: ActorResult[], + ): Promise { + const profiles = await this.profiles([result], viewer, opts) + return profiles[result.did] ?? null + } + + // @NOTE keep in sync with feedService.getActorViews() + async profilesBasic( + results: ActorResult[], viewer: string, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise - async profileBasic( - result: ActorResult | ActorResult[], + ): Promise> { + if (results.length === 0) return {} + const profiles = await this.profiles(results, viewer, opts) + return Object.values(profiles).reduce((acc, cur) => { + const profile = { + did: cur.did, + handle: cur.handle, + displayName: truncateUtf8(cur.displayName, 64) || undefined, + avatar: cur.avatar, + viewer: cur.viewer, + labels: cur.labels, + } + acc[cur.did] = profile + return acc + }, {} as Record) + } + + async hydrateProfilesBasic( + results: ActorResult[], viewer: string, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const results = Array.isArray(result) ? result : [result] - if (results.length === 0) return [] - - const profiles = await this.profile(results, viewer, opts) - const views = profiles.map((view) => ({ - did: view.did, - handle: view.handle, - displayName: truncateUtf8(view.displayName, 64) || undefined, - avatar: view.avatar, - viewer: view.viewer, - labels: view.labels, - })) + ): Promise { + const profiles = await this.profilesBasic(results, viewer, opts) + return mapDefined(results, (result) => profiles[result.did]) + } - return Array.isArray(result) ? views : views[0] + async profileBasic( + result: ActorResult, + viewer: string, + opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, + ): Promise { + const profiles = await this.profilesBasic([result], viewer, opts) + return profiles[result.did] ?? null } } diff --git a/packages/pds/src/app-view/services/feed/index.ts b/packages/pds/src/app-view/services/feed/index.ts index 3f1fc54a799..d26f43d3702 100644 --- a/packages/pds/src/app-view/services/feed/index.ts +++ b/packages/pds/src/app-view/services/feed/index.ts @@ -192,22 +192,25 @@ export class FeedService { ) return actors.reduce((acc, cur) => { const actorLabels = labels[cur.did] ?? [] + const avatar = cur.avatarCid + ? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid) + : undefined + const mutedByList = + cur.requesterMutedByList && listViews[cur.requesterMutedByList] + ? this.services.graph.formatListViewBasic( + listViews[cur.requesterMutedByList], + ) + : undefined return { ...acc, [cur.did]: { did: cur.did, handle: cur.handle, displayName: truncateUtf8(cur.displayName, 64) || undefined, - avatar: cur.avatarCid - ? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid) - : undefined, + avatar, viewer: { muted: !!cur?.requesterMuted || !!cur?.requesterMutedByList, - mutedByList: cur.requesterMutedByList - ? this.services.graph.formatListViewBasic( - listViews[cur.requesterMutedByList], - ) - : undefined, + mutedByList, blockedBy: !!cur?.requesterBlockedBy, blocking: cur?.requesterBlocking || undefined, following: cur?.requesterFollowing || undefined, From 30b41029c11d33cdded4f1ba8df4290465dfb75e Mon Sep 17 00:00:00 2001 From: devin ivy Date: Tue, 1 Aug 2023 14:59:00 -0400 Subject: [PATCH 078/237] Partitioning in bsky indexer (#1368) * setup redis infra for appview indexer * barebones bsky ingester * add ioredis to bsky * remove some indexer functionality from bsky api * setup for bsky indexer * tidy * tidy, observe basic pipeline functioning * process messages on bsky indexer pipeline, tidy tests and lifecycle * trim partitions when moving cursor * simplify config for partitions * misc fixes for redis setup in bsky tests, add namespacing * fix pds proxy tests * remove cursor state from indexer partitions, simplify ingester state * tidy * utils for testing w/ multiple indexers, fix off-by-one xtrim * test reingesting * test indexer repartitioning * add entrypoints for bsky ingester and indexer, fix db schema config, api entrypoint name, tidy * setup and test bsky ingester backpressure, add config * tidy * add missing test file * tidy redis calls, add redis sentinel config * tidy/test some utils used in bsky pipeline * tidy bsky pipeline tests, move helpers into dev-env * fix pds crud test * support redis host and password config * better loggin/observability in ingester and indexer, make build * add bsky ingester initial cursor config * temporarily remove migrations from indexer/ingester * allow ingester to batch * packages/pg becomes packages/dev-infra with some cleanup (#1402) * packages/dev-infra/ * Extract packages/dev-infra/_common.sh and use it * Use period instead of source because of /bin/sh * add docs for redis test script * fix repartition test * add logs to debug ci * simplify repartitioning test, remove ci logs --------- Co-authored-by: Jerry Chen --- .../workflows/build-and-push-bsky-aws.yaml | 1 + package.json | 4 +- packages/bsky/Dockerfile | 2 +- packages/bsky/package.json | 3 +- packages/bsky/service/{index.js => api.js} | 4 +- packages/bsky/service/indexer.js | 80 +++ packages/bsky/service/ingester.js | 76 +++ packages/bsky/src/config.ts | 38 -- packages/bsky/src/context.ts | 6 - packages/bsky/src/index.ts | 74 +-- packages/bsky/src/indexer/config.ts | 186 +++++++ packages/bsky/src/indexer/context.ts | 51 ++ packages/bsky/src/indexer/index.ts | 112 +++++ packages/bsky/src/indexer/logger.ts | 3 + packages/bsky/src/indexer/services.ts | 25 + packages/bsky/src/indexer/subscription.ts | 355 ++++++++++++++ packages/bsky/src/ingester/config.ts | 141 ++++++ packages/bsky/src/ingester/context.ts | 27 ++ packages/bsky/src/ingester/index.ts | 79 +++ packages/bsky/src/ingester/logger.ts | 3 + packages/bsky/src/ingester/subscription.ts | 288 +++++++++++ packages/bsky/src/labeler/base.ts | 4 +- packages/bsky/src/labeler/hive.ts | 4 +- packages/bsky/src/labeler/keyword.ts | 4 +- packages/bsky/src/redis.ts | 149 ++++++ packages/bsky/src/services/index.ts | 17 +- packages/bsky/src/subscription/repo.ts | 457 ------------------ packages/bsky/src/subscription/util.ts | 54 +++ packages/bsky/tests/algos/hot-classic.test.ts | 4 +- packages/bsky/tests/algos/whats-hot.test.ts | 4 +- .../bsky/tests/algos/with-friends.test.ts | 4 +- packages/bsky/tests/blob-resolver.test.ts | 2 +- packages/bsky/tests/did-cache.test.ts | 4 +- packages/bsky/tests/duplicate-records.test.ts | 6 +- packages/bsky/tests/feed-generation.test.ts | 6 +- .../bsky/tests/handle-invalidation.test.ts | 6 +- packages/bsky/tests/image/server.test.ts | 2 +- packages/bsky/tests/indexing.test.ts | 45 +- packages/bsky/tests/labeler/labeler.test.ts | 12 +- .../bsky/tests/pipeline/backpressure.test.ts | 69 +++ packages/bsky/tests/pipeline/reingest.test.ts | 52 ++ .../bsky/tests/pipeline/repartition.test.ts | 87 ++++ packages/bsky/tests/server.test.ts | 3 +- packages/bsky/tests/subscription/repo.test.ts | 14 +- .../bsky/tests/views/actor-search.test.ts | 6 +- .../tests/views/admin/repo-search.test.ts | 6 +- packages/bsky/tests/views/author-feed.test.ts | 1 - packages/bsky/tests/views/follows.test.ts | 2 +- packages/bsky/tests/views/mute-lists.test.ts | 3 +- .../bsky/tests/views/notifications.test.ts | 6 +- packages/bsky/tests/views/posts.test.ts | 2 +- packages/bsky/tests/views/profile.test.ts | 2 +- packages/bsky/tests/views/suggestions.test.ts | 2 +- packages/bsky/tests/views/thread.test.ts | 6 +- packages/bsky/tests/views/timeline.test.ts | 2 +- packages/common/src/buffers.ts | 10 + packages/common/src/index.ts | 1 + packages/crypto/tests/random.test.ts | 15 + packages/dev-env/src/bsky.ts | 228 ++++++++- packages/dev-env/src/network.ts | 15 +- packages/dev-env/src/types.ts | 1 + packages/dev-env/src/util.ts | 9 +- packages/{pg => dev-infra}/README.md | 22 +- packages/dev-infra/_common.sh | 92 ++++ .../{pg => dev-infra}/docker-compose.yaml | 22 + packages/dev-infra/with-test-db.sh | 9 + packages/dev-infra/with-test-redis-and-db.sh | 10 + packages/pds/package.json | 4 +- packages/pds/tests/crud.test.ts | 11 +- packages/pg/with-test-db.sh | 46 -- yarn.lock | 57 +++ 71 files changed, 2412 insertions(+), 745 deletions(-) rename packages/bsky/service/{index.js => api.js} (97%) create mode 100644 packages/bsky/service/indexer.js create mode 100644 packages/bsky/service/ingester.js create mode 100644 packages/bsky/src/indexer/config.ts create mode 100644 packages/bsky/src/indexer/context.ts create mode 100644 packages/bsky/src/indexer/index.ts create mode 100644 packages/bsky/src/indexer/logger.ts create mode 100644 packages/bsky/src/indexer/services.ts create mode 100644 packages/bsky/src/indexer/subscription.ts create mode 100644 packages/bsky/src/ingester/config.ts create mode 100644 packages/bsky/src/ingester/context.ts create mode 100644 packages/bsky/src/ingester/index.ts create mode 100644 packages/bsky/src/ingester/logger.ts create mode 100644 packages/bsky/src/ingester/subscription.ts create mode 100644 packages/bsky/src/redis.ts delete mode 100644 packages/bsky/src/subscription/repo.ts create mode 100644 packages/bsky/tests/pipeline/backpressure.test.ts create mode 100644 packages/bsky/tests/pipeline/reingest.test.ts create mode 100644 packages/bsky/tests/pipeline/repartition.test.ts create mode 100644 packages/common/src/buffers.ts create mode 100644 packages/crypto/tests/random.test.ts rename packages/{pg => dev-infra}/README.md (67%) create mode 100755 packages/dev-infra/_common.sh rename packages/{pg => dev-infra}/docker-compose.yaml (51%) create mode 100755 packages/dev-infra/with-test-db.sh create mode 100755 packages/dev-infra/with-test-redis-and-db.sh delete mode 100755 packages/pg/with-test-db.sh diff --git a/.github/workflows/build-and-push-bsky-aws.yaml b/.github/workflows/build-and-push-bsky-aws.yaml index b656dec89c9..09005d43a07 100644 --- a/.github/workflows/build-and-push-bsky-aws.yaml +++ b/.github/workflows/build-and-push-bsky-aws.yaml @@ -3,6 +3,7 @@ on: push: branches: - main + - indexer-redis-setup env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} diff --git a/package.json b/package.json index 2c1337435b3..45fbd5ad7ae 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "verify": "lerna run verify --stream", "prettier": "lerna run prettier", "build": "lerna run build", - "test": "LOG_ENABLED=false NODE_ENV=development ./packages/pg/with-test-db.sh lerna run test --stream", - "test:withFlags": "LOG_ENABLED=false NODE_ENV=development ./packages/pg/with-test-db.sh lerna run test --stream --" + "test": "LOG_ENABLED=false NODE_ENV=development ./packages/dev-infra/with-test-db.sh lerna run test --stream", + "test:withFlags": "LOG_ENABLED=false NODE_ENV=development ./packages/dev-infra/with-test-db.sh lerna run test --stream --" }, "devDependencies": { "@babel/core": "^7.18.6", diff --git a/packages/bsky/Dockerfile b/packages/bsky/Dockerfile index 04feb758ac2..43028908da4 100644 --- a/packages/bsky/Dockerfile +++ b/packages/bsky/Dockerfile @@ -44,7 +44,7 @@ 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", "index.js"] +CMD ["node", "--enable-source-maps", "api.js"] LABEL org.opencontainers.image.source=https://github.com/bluesky-social/atproto LABEL org.opencontainers.image.description="Bsky App View" diff --git a/packages/bsky/package.json b/packages/bsky/package.json index c2144154d57..c16ec6c1d70 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -14,7 +14,7 @@ "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "start": "node --enable-source-maps dist/bin.js", - "test": "../pg/with-test-db.sh jest", + "test": "../dev-infra/with-test-redis-and-db.sh jest", "test:log": "tail -50 test.log | pino-pretty", "test:updateSnapshot": "jest --updateSnapshot", "prettier": "prettier --check src/ tests/", @@ -47,6 +47,7 @@ "express-async-errors": "^3.1.1", "http-errors": "^2.0.0", "http-terminator": "^3.2.0", + "ioredis": "^5.3.2", "kysely": "^0.22.0", "multiformats": "^9.6.4", "p-queue": "^6.6.2", diff --git a/packages/bsky/service/index.js b/packages/bsky/service/api.js similarity index 97% rename from packages/bsky/service/index.js rename to packages/bsky/service/api.js index 85cdc832146..e27221526d7 100644 --- a/packages/bsky/service/index.js +++ b/packages/bsky/service/api.js @@ -33,7 +33,7 @@ const main = async () => { // Use lower-credentialed user to run the app const db = Database.postgres({ url: env.dbPostgresUrl, - schema: env.dbSchema, + schema: env.dbPostgresSchema, poolSize: env.dbPoolSize, poolMaxUses: env.dbPoolMaxUses, poolIdleTimeoutMs: env.dbPoolIdleTimeoutMs, @@ -41,7 +41,6 @@ const main = async () => { const cfg = ServerConfig.readEnv({ port: env.port, version: env.version, - repoProvider: env.repoProvider, dbPostgresUrl: env.dbPostgresUrl, dbPostgresSchema: env.dbPostgresSchema, publicUrl: env.publicUrl, @@ -79,7 +78,6 @@ const main = async () => { const getEnv = () => ({ port: parseInt(process.env.PORT), version: process.env.BSKY_VERSION, - repoProvider: process.env.REPO_PROVIDER, dbPostgresUrl: process.env.DB_POSTGRES_URL, dbMigratePostgresUrl: process.env.DB_MIGRATE_POSTGRES_URL || process.env.DB_POSTGRES_URL, diff --git a/packages/bsky/service/indexer.js b/packages/bsky/service/indexer.js new file mode 100644 index 00000000000..f7071dd9de8 --- /dev/null +++ b/packages/bsky/service/indexer.js @@ -0,0 +1,80 @@ +'use strict' /* eslint-disable */ + +require('dd-trace/init') // Only works with commonjs + +// Tracer code above must come before anything else +const { Database, IndexerConfig, BskyIndexer, Redis } = require('@atproto/bsky') + +const main = async () => { + const env = getEnv() + // Migrate using credentialed user + // @TODO temporarily disabled for testing purposes + // const migrateDb = Database.postgres({ + // url: env.dbMigratePostgresUrl, + // schema: env.dbPostgresSchema, + // poolSize: 2, + // }) + // await migrateDb.migrateToLatestOrThrow() + // await migrateDb.close() + const db = Database.postgres({ + url: env.dbPostgresUrl, + schema: env.dbPostgresSchema, + poolSize: env.dbPoolSize, + poolMaxUses: env.dbPoolMaxUses, + poolIdleTimeoutMs: env.dbPoolIdleTimeoutMs, + }) + const cfg = IndexerConfig.readEnv({ + version: env.version, + dbPostgresUrl: env.dbPostgresUrl, + dbPostgresSchema: env.dbPostgresSchema, + }) + const redis = new Redis( + cfg.redisSentinelName + ? { + sentinel: cfg.redisSentinelName, + hosts: cfg.redisSentinelHosts, + password: cfg.redisPassword, + } + : { + host: cfg.redisHost, + password: cfg.redisPassword, + }, + ) + const indexer = BskyIndexer.create({ db, redis, cfg }) + await indexer.start() + process.on('SIGTERM', async () => { + await indexer.destroy() + }) +} + +// Also accepts the following in readEnv(): +// - REDIS_HOST +// - REDIS_SENTINEL_NAME +// - REDIS_SENTINEL_HOSTS +// - REDIS_PASSWORD +// - DID_PLC_URL +// - DID_CACHE_STALE_TTL +// - DID_CACHE_MAX_TTL +// - LABELER_DID +// - HIVE_API_KEY +// - INDEXER_PARTITION_IDS +// - INDEXER_PARTITION_BATCH_SIZE +// - INDEXER_CONCURRENCY +// - INDEXER_SUB_LOCK_ID +const getEnv = () => ({ + version: process.env.BSKY_VERSION, + dbPostgresUrl: process.env.DB_POSTGRES_URL, + dbMigratePostgresUrl: + process.env.DB_MIGRATE_POSTGRES_URL || process.env.DB_POSTGRES_URL, + dbPostgresSchema: process.env.DB_POSTGRES_SCHEMA || undefined, + dbPoolSize: maybeParseInt(process.env.DB_POOL_SIZE), + dbPoolMaxUses: maybeParseInt(process.env.DB_POOL_MAX_USES), + dbPoolIdleTimeoutMs: maybeParseInt(process.env.DB_POOL_IDLE_TIMEOUT_MS), +}) + +const maybeParseInt = (str) => { + const parsed = parseInt(str) + return isNaN(parsed) ? undefined : parsed +} + +main() diff --git a/packages/bsky/service/ingester.js b/packages/bsky/service/ingester.js new file mode 100644 index 00000000000..35d7ecd9979 --- /dev/null +++ b/packages/bsky/service/ingester.js @@ -0,0 +1,76 @@ +'use strict' /* eslint-disable */ + +require('dd-trace/init') // Only works with commonjs + +// Tracer code above must come before anything else +const { + Database, + IngesterConfig, + BskyIngester, + Redis, +} = require('@atproto/bsky') + +const main = async () => { + const env = getEnv() + // No migration: ingester only uses pg for a lock + const db = Database.postgres({ + url: env.dbPostgresUrl, + schema: env.dbPostgresSchema, + poolSize: env.dbPoolSize, + poolMaxUses: env.dbPoolMaxUses, + poolIdleTimeoutMs: env.dbPoolIdleTimeoutMs, + }) + const cfg = IngesterConfig.readEnv({ + version: env.version, + dbPostgresUrl: env.dbPostgresUrl, + dbPostgresSchema: env.dbPostgresSchema, + repoProvider: env.repoProvider, + ingesterSubLockId: env.subLockId, + }) + const redis = new Redis( + cfg.redisSentinelName + ? { + sentinel: cfg.redisSentinelName, + hosts: cfg.redisSentinelHosts, + password: cfg.redisPassword, + } + : { + host: cfg.redisHost, + password: cfg.redisPassword, + }, + ) + const ingester = BskyIngester.create({ db, redis, cfg }) + await ingester.start() + process.on('SIGTERM', async () => { + await ingester.destroy() + }) +} + +// Also accepts the following in readEnv(): +// - REDIS_HOST +// - REDIS_SENTINEL_NAME +// - REDIS_SENTINEL_HOSTS +// - REDIS_PASSWORD +// - REPO_PROVIDER +// - INGESTER_PARTITION_COUNT +// - INGESTER_MAX_ITEMS +// - INGESTER_CHECK_ITEMS_EVERY_N +// - INGESTER_INITIAL_CURSOR +// - INGESTER_SUB_LOCK_ID +const getEnv = () => ({ + version: process.env.BSKY_VERSION, + dbPostgresUrl: process.env.DB_POSTGRES_URL, + dbMigratePostgresUrl: + process.env.DB_MIGRATE_POSTGRES_URL || process.env.DB_POSTGRES_URL, + dbPostgresSchema: process.env.DB_POSTGRES_SCHEMA || undefined, + dbPoolSize: maybeParseInt(process.env.DB_POOL_SIZE), + dbPoolMaxUses: maybeParseInt(process.env.DB_POOL_MAX_USES), + dbPoolIdleTimeoutMs: maybeParseInt(process.env.DB_POOL_IDLE_TIMEOUT_MS), +}) + +const maybeParseInt = (str) => { + const parsed = parseInt(str) + return isNaN(parsed) ? undefined : parsed +} + +main() diff --git a/packages/bsky/src/config.ts b/packages/bsky/src/config.ts index 83081a2a1ad..560f9cc02a5 100644 --- a/packages/bsky/src/config.ts +++ b/packages/bsky/src/config.ts @@ -17,15 +17,10 @@ export interface ServerConfigValues { imgUriKey: string imgUriEndpoint?: string blobCacheLocation?: string - repoProvider?: string - repoSubLockId?: number labelerDid: string - hiveApiKey?: string adminPassword: string moderatorPassword?: string triagePassword?: string - labelerKeywords: Record - indexerConcurrency?: number } export class ServerConfig { @@ -60,14 +55,10 @@ export class ServerConfig { overrides?.dbPostgresUrl || process.env.DB_POSTGRES_URL assert(dbPostgresUrl) const dbPostgresSchema = process.env.DB_POSTGRES_SCHEMA - const repoProvider = process.env.REPO_PROVIDER // E.g. ws://abc.com:4000 const adminPassword = process.env.ADMIN_PASSWORD || 'admin' const moderatorPassword = process.env.MODERATOR_PASSWORD || undefined const triagePassword = process.env.TRIAGE_PASSWORD || undefined const labelerDid = process.env.LABELER_DID || 'did:example:labeler' - const hiveApiKey = process.env.HIVE_API_KEY || undefined - const indexerConcurrency = maybeParseInt(process.env.INDEXER_CONCURRENCY) - const labelerKeywords = {} return new ServerConfig({ version, debugMode, @@ -84,14 +75,10 @@ export class ServerConfig { imgUriKey, imgUriEndpoint, blobCacheLocation, - repoProvider, labelerDid, - hiveApiKey, adminPassword, moderatorPassword, triagePassword, - labelerKeywords, - indexerConcurrency, ...stripUndefineds(overrides ?? {}), }) } @@ -169,26 +156,10 @@ export class ServerConfig { return this.cfg.blobCacheLocation } - get repoProvider() { - return this.cfg.repoProvider - } - - get repoSubLockId() { - return this.cfg.repoSubLockId - } - get labelerDid() { return this.cfg.labelerDid } - get hiveApiKey() { - return this.cfg.hiveApiKey - } - - get labelerKeywords() { - return this.cfg.labelerKeywords - } - get adminPassword() { return this.cfg.adminPassword } @@ -200,10 +171,6 @@ export class ServerConfig { get triagePassword() { return this.cfg.triagePassword } - - get indexerConcurrency() { - return this.cfg.indexerConcurrency - } } function stripUndefineds( @@ -217,8 +184,3 @@ function stripUndefineds( }) return result } - -function maybeParseInt(str) { - const parsed = parseInt(str) - return isNaN(parsed) ? undefined : parsed -} diff --git a/packages/bsky/src/context.ts b/packages/bsky/src/context.ts index a3761321b62..f5933cdcf1c 100644 --- a/packages/bsky/src/context.ts +++ b/packages/bsky/src/context.ts @@ -6,7 +6,6 @@ import { ImageUriBuilder } from './image/uri' import { Services } from './services' import * as auth from './auth' import DidSqlCache from './did-cache' -import { Labeler } from './labeler' import { BackgroundQueue } from './background' import { MountedAlgos } from './feed-gen/types' @@ -19,7 +18,6 @@ export class AppContext { services: Services idResolver: IdResolver didCache: DidSqlCache - labeler: Labeler backgroundQueue: BackgroundQueue algos: MountedAlgos }, @@ -71,10 +69,6 @@ export class AppContext { return auth.roleVerifier(this.cfg) } - get labeler(): Labeler { - return this.opts.labeler - } - get backgroundQueue(): BackgroundQueue { return this.opts.backgroundQueue } diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 537c30e4d80..86ac01b8fd8 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -9,20 +9,18 @@ import { IdResolver } from '@atproto/identity' import API, { health, blobResolver } from './api' import Database from './db' import * as error from './error' -import { dbLogger, loggerMiddleware, subLogger } from './logger' +import { dbLogger, loggerMiddleware } from './logger' import { ServerConfig } from './config' import { createServer } from './lexicon' import { ImageUriBuilder } from './image/uri' import { BlobDiskCache, ImageProcessingServer } from './image/server' import { createServices } from './services' import AppContext from './context' -import { RepoSubscription } from './subscription/repo' import DidSqlCache from './did-cache' import { ImageInvalidator, ImageProcessingServerInvalidator, } from './image/invalidator' -import { HiveLabeler, KeywordLabeler, Labeler } from './labeler' import { BackgroundQueue } from './background' import { MountedAlgos } from './feed-gen/types' @@ -30,27 +28,23 @@ export type { ServerConfigValues } from './config' export type { MountedAlgos } from './feed-gen/types' export { ServerConfig } from './config' export { Database } from './db' +export { Redis } from './redis' export { ViewMaintainer } from './db/views' export { AppContext } from './context' export { makeAlgos } from './feed-gen' +export * from './indexer' +export * from './ingester' export class BskyAppView { public ctx: AppContext public app: express.Application - public sub?: RepoSubscription public server?: http.Server private terminator?: HttpTerminator private dbStatsInterval: NodeJS.Timer - private subStatsInterval: NodeJS.Timer - constructor(opts: { - ctx: AppContext - app: express.Application - sub?: RepoSubscription - }) { + constructor(opts: { ctx: AppContext; app: express.Application }) { this.ctx = opts.ctx this.app = opts.app - this.sub = opts.sub } static create(opts: { @@ -100,31 +94,7 @@ export class BskyAppView { const backgroundQueue = new BackgroundQueue(db) - // @TODO background labeling tasks - let labeler: Labeler - if (config.hiveApiKey) { - labeler = new HiveLabeler(config.hiveApiKey, { - db, - cfg: config, - idResolver, - backgroundQueue, - }) - } else { - labeler = new KeywordLabeler({ - db, - cfg: config, - idResolver, - backgroundQueue, - }) - } - - const services = createServices({ - imgUriBuilder, - imgInvalidator, - idResolver, - labeler, - backgroundQueue, - }) + const services = createServices({ imgUriBuilder, imgInvalidator }) const ctx = new AppContext({ db, @@ -133,7 +103,6 @@ export class BskyAppView { imgUriBuilder, idResolver, didCache, - labeler, backgroundQueue, algos, }) @@ -157,16 +126,7 @@ export class BskyAppView { app.use(server.xrpc.router) app.use(error.handler) - const sub = config.repoProvider - ? new RepoSubscription( - ctx, - config.repoProvider, - config.repoSubLockId, - config.indexerConcurrency, - ) - : undefined - - return new BskyAppView({ ctx, app, sub }) + return new BskyAppView({ ctx, app }) } async start(): Promise { @@ -189,19 +149,6 @@ export class BskyAppView { 'background queue stats', ) }, 10000) - if (this.sub) { - this.subStatsInterval = setInterval(() => { - subLogger.info( - { - seq: this.sub?.lastSeq, - cursor: this.sub?.lastCursor, - runningCount: this.sub?.repoQueue.main.pending, - waitingCount: this.sub?.repoQueue.main.size, - }, - 'repo subscription stats', - ) - }, 500) - } const server = this.app.listen(this.ctx.cfg.port) this.server = server server.keepAliveTimeout = 90000 @@ -209,17 +156,14 @@ export class BskyAppView { await events.once(server, 'listening') const { port } = server.address() as AddressInfo this.ctx.cfg.assignPort(port) - this.sub?.run() // Don't await, backgrounded return server } - async destroy(): Promise { + async destroy(opts?: { skipDb: boolean }): Promise { await this.ctx.didCache.destroy() - await this.sub?.destroy() - clearInterval(this.subStatsInterval) await this.terminator?.terminate() await this.ctx.backgroundQueue.destroy() - await this.ctx.db.close() + if (!opts?.skipDb) await this.ctx.db.close() clearInterval(this.dbStatsInterval) } } diff --git a/packages/bsky/src/indexer/config.ts b/packages/bsky/src/indexer/config.ts new file mode 100644 index 00000000000..04efee208e0 --- /dev/null +++ b/packages/bsky/src/indexer/config.ts @@ -0,0 +1,186 @@ +import assert from 'assert' +import { DAY, HOUR, parseIntWithFallback } from '@atproto/common' + +export interface IndexerConfigValues { + version: string + dbPostgresUrl: string + dbPostgresSchema?: string + redisHost?: string // either set redis host, or both sentinel name and hosts + redisSentinelName?: string + redisSentinelHosts?: string[] + redisPassword?: string + didPlcUrl: string + didCacheStaleTTL: number + didCacheMaxTTL: number + labelerDid: string + hiveApiKey?: string + labelerKeywords: Record + indexerConcurrency?: number + indexerPartitionIds: number[] + indexerPartitionBatchSize?: number + indexerSubLockId?: number + indexerNamespace?: string +} + +export class IndexerConfig { + constructor(private cfg: IndexerConfigValues) {} + + static readEnv(overrides?: Partial) { + const version = process.env.BSKY_VERSION || '0.0.0' + const dbPostgresUrl = + overrides?.dbPostgresUrl || process.env.DB_POSTGRES_URL + const dbPostgresSchema = + overrides?.dbPostgresSchema || process.env.DB_POSTGRES_SCHEMA + const redisHost = + overrides?.redisHost || process.env.REDIS_HOST || undefined + const redisSentinelName = + overrides?.redisSentinelName || + process.env.REDIS_SENTINEL_NAME || + undefined + const redisSentinelHosts = + overrides?.redisSentinelHosts || + (process.env.REDIS_SENTINEL_HOSTS + ? process.env.REDIS_SENTINEL_HOSTS.split(',') + : []) + const redisPassword = + overrides?.redisPassword || process.env.REDIS_PASSWORD || undefined + const didPlcUrl = process.env.DID_PLC_URL || 'http://localhost:2582' + const didCacheStaleTTL = parseIntWithFallback( + process.env.DID_CACHE_STALE_TTL, + HOUR, + ) + const didCacheMaxTTL = parseIntWithFallback( + process.env.DID_CACHE_MAX_TTL, + DAY, + ) + const labelerDid = process.env.LABELER_DID || 'did:example:labeler' + const hiveApiKey = process.env.HIVE_API_KEY || undefined + const indexerPartitionIds = + overrides?.indexerPartitionIds || + (process.env.INDEXER_PARTITION_IDS + ? process.env.INDEXER_PARTITION_IDS.split(',').map((n) => + parseInt(n, 10), + ) + : []) + const indexerPartitionBatchSize = maybeParseInt( + process.env.INDEXER_PARTITION_BATCH_SIZE, + ) + const indexerConcurrency = maybeParseInt(process.env.INDEXER_CONCURRENCY) + const indexerNamespace = overrides?.indexerNamespace + const indexerSubLockId = maybeParseInt(process.env.INDEXER_SUB_LOCK_ID) + const labelerKeywords = {} + assert(dbPostgresUrl) + assert(redisHost || (redisSentinelName && redisSentinelHosts?.length)) + assert(indexerPartitionIds.length > 0) + return new IndexerConfig({ + version, + dbPostgresUrl, + dbPostgresSchema, + redisHost, + redisSentinelName, + redisSentinelHosts, + redisPassword, + didPlcUrl, + didCacheStaleTTL, + didCacheMaxTTL, + labelerDid, + hiveApiKey, + indexerPartitionIds, + indexerConcurrency, + indexerPartitionBatchSize, + indexerNamespace, + indexerSubLockId, + labelerKeywords, + ...stripUndefineds(overrides ?? {}), + }) + } + + get version() { + return this.cfg.version + } + + get dbPostgresUrl() { + return this.cfg.dbPostgresUrl + } + + get dbPostgresSchema() { + return this.cfg.dbPostgresSchema + } + + get redisHost() { + return this.cfg.redisHost + } + + get redisSentinelName() { + return this.cfg.redisSentinelName + } + + get redisSentinelHosts() { + return this.cfg.redisSentinelHosts + } + + get redisPassword() { + return this.cfg.redisPassword + } + + get didPlcUrl() { + return this.cfg.didPlcUrl + } + + get didCacheStaleTTL() { + return this.cfg.didCacheStaleTTL + } + + get didCacheMaxTTL() { + return this.cfg.didCacheMaxTTL + } + + get labelerDid() { + return this.cfg.labelerDid + } + + get hiveApiKey() { + return this.cfg.hiveApiKey + } + + get indexerConcurrency() { + return this.cfg.indexerConcurrency + } + + get indexerPartitionIds() { + return this.cfg.indexerPartitionIds + } + + get indexerPartitionBatchSize() { + return this.cfg.indexerPartitionBatchSize + } + + get indexerNamespace() { + return this.cfg.indexerNamespace + } + + get indexerSubLockId() { + return this.cfg.indexerSubLockId + } + + get labelerKeywords() { + return this.cfg.labelerKeywords + } +} + +function stripUndefineds( + obj: Record, +): Record { + const result = {} + Object.entries(obj).forEach(([key, val]) => { + if (val !== undefined) { + result[key] = val + } + }) + return result +} + +function maybeParseInt(str) { + const parsed = parseInt(str) + return isNaN(parsed) ? undefined : parsed +} diff --git a/packages/bsky/src/indexer/context.ts b/packages/bsky/src/indexer/context.ts new file mode 100644 index 00000000000..e8f9b6abae5 --- /dev/null +++ b/packages/bsky/src/indexer/context.ts @@ -0,0 +1,51 @@ +import { IdResolver } from '@atproto/identity' +import { Database } from '../db' +import { IndexerConfig } from './config' +import { Services } from './services' +import { BackgroundQueue } from '../background' +import DidSqlCache from '../did-cache' +import { Redis } from '../redis' + +export class IndexerContext { + constructor( + private opts: { + db: Database + redis: Redis + cfg: IndexerConfig + services: Services + idResolver: IdResolver + didCache: DidSqlCache + backgroundQueue: BackgroundQueue + }, + ) {} + + get db(): Database { + return this.opts.db + } + + get redis(): Redis { + return this.opts.redis + } + + get cfg(): IndexerConfig { + return this.opts.cfg + } + + get services(): Services { + return this.opts.services + } + + get idResolver(): IdResolver { + return this.opts.idResolver + } + + get didCache(): DidSqlCache { + return this.opts.didCache + } + + get backgroundQueue(): BackgroundQueue { + return this.opts.backgroundQueue + } +} + +export default IndexerContext diff --git a/packages/bsky/src/indexer/index.ts b/packages/bsky/src/indexer/index.ts new file mode 100644 index 00000000000..c50654975a4 --- /dev/null +++ b/packages/bsky/src/indexer/index.ts @@ -0,0 +1,112 @@ +import { IdResolver } from '@atproto/identity' +import { BackgroundQueue } from '../background' +import Database from '../db' +import DidSqlCache from '../did-cache' +import log from './logger' +import { dbLogger } from '../logger' +import { IndexerConfig } from './config' +import { IndexerContext } from './context' +import { createServices } from './services' +import { IndexerSubscription } from './subscription' +import { HiveLabeler, KeywordLabeler, Labeler } from '../labeler' +import { Redis } from '../redis' + +export { IndexerConfig } from './config' +export type { IndexerConfigValues } from './config' + +export class BskyIndexer { + public ctx: IndexerContext + public sub: IndexerSubscription + private dbStatsInterval: NodeJS.Timer + private subStatsInterval: NodeJS.Timer + + constructor(opts: { ctx: IndexerContext; sub: IndexerSubscription }) { + this.ctx = opts.ctx + this.sub = opts.sub + } + + static create(opts: { + db: Database + redis: Redis + cfg: IndexerConfig + }): BskyIndexer { + const { db, redis, cfg } = opts + const didCache = new DidSqlCache( + db, + cfg.didCacheStaleTTL, + cfg.didCacheMaxTTL, + ) + const idResolver = new IdResolver({ plcUrl: cfg.didPlcUrl, didCache }) + const backgroundQueue = new BackgroundQueue(db) + let labeler: Labeler + if (cfg.hiveApiKey) { + labeler = new HiveLabeler(cfg.hiveApiKey, { + db, + cfg, + idResolver, + backgroundQueue, + }) + } else { + labeler = new KeywordLabeler({ + db, + cfg, + idResolver, + backgroundQueue, + }) + } + const services = createServices({ idResolver, labeler, backgroundQueue }) + const ctx = new IndexerContext({ + db, + redis, + cfg, + services, + idResolver, + didCache, + backgroundQueue, + }) + const sub = new IndexerSubscription(ctx, { + partitionIds: cfg.indexerPartitionIds, + partitionBatchSize: cfg.indexerPartitionBatchSize, + concurrency: cfg.indexerConcurrency, + subLockId: cfg.indexerSubLockId, + }) + return new BskyIndexer({ ctx, sub }) + } + + async start() { + const { db } = this.ctx + const { pool } = db.cfg + this.dbStatsInterval = setInterval(() => { + dbLogger.info( + { + idleCount: pool.idleCount, + totalCount: pool.totalCount, + waitingCount: pool.waitingCount, + }, + 'db pool stats', + ) + }, 10000) + this.subStatsInterval = setInterval(() => { + log.info( + { + processedCount: this.sub.processedCount, + runningCount: this.sub.repoQueue.main.pending, + waitingCount: this.sub.repoQueue.main.size, + }, + 'indexer stats', + ) + }, 500) + this.sub.run() + return this + } + + async destroy(opts?: { skipDb: boolean; skipRedis: true }): Promise { + await this.sub.destroy() + clearInterval(this.subStatsInterval) + if (!opts?.skipRedis) await this.ctx.redis.destroy() + if (!opts?.skipDb) await this.ctx.db.close() + clearInterval(this.dbStatsInterval) + } +} + +export default BskyIndexer diff --git a/packages/bsky/src/indexer/logger.ts b/packages/bsky/src/indexer/logger.ts new file mode 100644 index 00000000000..dc981563732 --- /dev/null +++ b/packages/bsky/src/indexer/logger.ts @@ -0,0 +1,3 @@ +import { subsystemLogger } from '@atproto/common' + +export default subsystemLogger('bsky:indexer') diff --git a/packages/bsky/src/indexer/services.ts b/packages/bsky/src/indexer/services.ts new file mode 100644 index 00000000000..2cd2030287c --- /dev/null +++ b/packages/bsky/src/indexer/services.ts @@ -0,0 +1,25 @@ +import { IdResolver } from '@atproto/identity' +import Database from '../db' +import { Labeler } from '../labeler' +import { BackgroundQueue } from '../background' +import { IndexingService } from '../services/indexing' +import { LabelService } from '../services/label' + +export function createServices(resources: { + idResolver: IdResolver + labeler: Labeler + backgroundQueue: BackgroundQueue +}): Services { + const { idResolver, labeler, backgroundQueue } = resources + return { + indexing: IndexingService.creator(idResolver, labeler, backgroundQueue), + label: LabelService.creator(), + } +} + +export type Services = { + indexing: FromDb + label: FromDb +} + +type FromDb = (db: Database) => T diff --git a/packages/bsky/src/indexer/subscription.ts b/packages/bsky/src/indexer/subscription.ts new file mode 100644 index 00000000000..e3914571bd4 --- /dev/null +++ b/packages/bsky/src/indexer/subscription.ts @@ -0,0 +1,355 @@ +import assert from 'node:assert' +import { CID } from 'multiformats/cid' +import { AtUri } from '@atproto/uri' +import { cborDecode, wait } from '@atproto/common' +import { DisconnectError } from '@atproto/xrpc-server' +import { + WriteOpAction, + readCarWithRoot, + cborToLexRecord, + def, + Commit, +} from '@atproto/repo' +import { ValidationError } from '@atproto/lexicon' +import * as message from '../lexicon/types/com/atproto/sync/subscribeRepos' +import { Leader } from '../db/leader' +import { IndexingService } from '../services/indexing' +import log from './logger' +import { + ConsecutiveItem, + ConsecutiveList, + LatestQueue, + PartitionedQueue, + PerfectMap, + ProcessableMessage, + jitter, + loggableMessage, + strToInt, +} from '../subscription/util' +import IndexerContext from './context' + +export const INDEXER_SUB_LOCK_ID = 1200 // need one per partition + +export class IndexerSubscription { + destroyed = false + leader = new Leader(this.opts.subLockId || INDEXER_SUB_LOCK_ID, this.ctx.db) + processedCount = 0 + repoQueue = new PartitionedQueue({ + concurrency: this.opts.concurrency ?? Infinity, + }) + partitions = new PerfectMap() + partitionIds = this.opts.partitionIds + indexingSvc: IndexingService + + constructor( + public ctx: IndexerContext, + public opts: { + partitionIds: number[] + subLockId?: number + concurrency?: number + partitionBatchSize?: number + }, + ) { + this.indexingSvc = ctx.services.indexing(ctx.db) + } + + async processEvents(opts: { signal: AbortSignal }) { + const done = () => this.destroyed || opts.signal.aborted + while (!done()) { + const results = await this.ctx.redis.readStreams( + this.partitionIds.map((id) => ({ + key: partitionKey(id), + cursor: this.partitions.get(id).cursor, + })), + { + blockMs: 1000, + count: this.opts.partitionBatchSize ?? 50, // events per stream + }, + ) + if (done()) break + for (const { key, messages } of results) { + const partition = this.partitions.get(partitionId(key)) + for (const msg of messages) { + const seq = strToInt(msg.cursor) + const envelope = getEnvelope(msg.contents) + partition.cursor = seq + const item = partition.consecutive.push(seq) + this.repoQueue.add(envelope.repo, async () => { + await this.handleMessage(partition, item, envelope) + }) + } + } + await this.repoQueue.main.onEmpty() // backpressure + } + } + + async run() { + while (!this.destroyed) { + try { + const { ran } = await this.leader.run(async ({ signal }) => { + // initialize cursors to 0 (read from beginning of stream) + for (const id of this.partitionIds) { + this.partitions.set(id, new Partition(id, 0)) + } + // process events + await this.processEvents({ signal }) + }) + if (ran && !this.destroyed) { + throw new Error('Indexer sub completed, but should be persistent') + } + } catch (err) { + log.error({ err }, 'indexer sub error') + } + if (!this.destroyed) { + await wait(5000 + jitter(1000)) // wait then try to become leader + } + } + } + + async destroy() { + this.destroyed = true + await this.repoQueue.destroy() + await Promise.all( + [...this.partitions.values()].map((p) => p.cursorQueue.destroy()), + ) + this.leader.destroy(new DisconnectError()) + } + + async resume() { + this.destroyed = false + this.partitions = new Map() + this.repoQueue = new PartitionedQueue({ + concurrency: this.opts.concurrency ?? Infinity, + }) + await this.run() + } + + private async handleMessage( + partition: Partition, + item: ConsecutiveItem, + envelope: Envelope, + ) { + const msg = envelope.event + try { + if (message.isCommit(msg)) { + await this.handleCommit(msg) + } else if (message.isHandle(msg)) { + await this.handleUpdateHandle(msg) + } else if (message.isTombstone(msg)) { + await this.handleTombstone(msg) + } else if (message.isMigrate(msg)) { + // Ignore migrations + } else { + const exhaustiveCheck: never = msg + throw new Error(`Unhandled message type: ${exhaustiveCheck['$type']}`) + } + } catch (err) { + // We log messages we can't process and move on: + // otherwise the cursor would get stuck on a poison message. + log.error( + { err, message: loggableMessage(msg) }, + 'indexer message processing error', + ) + } finally { + this.processedCount++ + const latest = item.complete().at(-1) + if (latest !== undefined) { + partition.cursorQueue + .add(async () => { + await this.ctx.redis.trimStream(partition.key, latest + 1) + }) + .catch((err) => { + log.error({ err }, 'indexer cursor error') + }) + } + } + } + + private async handleCommit(msg: message.Commit) { + const indexRecords = async () => { + const { root, rootCid, ops } = await getOps(msg) + if (msg.tooBig) { + await this.indexingSvc.indexRepo(msg.repo, rootCid.toString()) + await this.indexingSvc.setCommitLastSeen(root, msg) + return + } + if (msg.rebase) { + const needsReindex = await this.indexingSvc.checkCommitNeedsIndexing( + root, + ) + if (needsReindex) { + await this.indexingSvc.indexRepo(msg.repo, rootCid.toString()) + } + await this.indexingSvc.setCommitLastSeen(root, msg) + return + } + for (const op of ops) { + if (op.action === WriteOpAction.Delete) { + await this.indexingSvc.deleteRecord(op.uri) + } else { + try { + await this.indexingSvc.indexRecord( + op.uri, + op.cid, + op.record, + op.action, // create or update + msg.time, + ) + } catch (err) { + if (err instanceof ValidationError) { + log.warn( + { + did: msg.repo, + commit: msg.commit.toString(), + uri: op.uri.toString(), + cid: op.cid.toString(), + }, + 'skipping indexing of invalid record', + ) + } else { + log.error( + { + err, + did: msg.repo, + commit: msg.commit.toString(), + uri: op.uri.toString(), + cid: op.cid.toString(), + }, + 'skipping indexing due to error processing record', + ) + } + } + } + } + await this.indexingSvc.setCommitLastSeen(root, msg) + } + const results = await Promise.allSettled([ + indexRecords(), + this.indexingSvc.indexHandle(msg.repo, msg.time), + ]) + handleAllSettledErrors(results) + } + + private async handleUpdateHandle(msg: message.Handle) { + await this.indexingSvc.indexHandle(msg.did, msg.time, true) + } + + private async handleTombstone(msg: message.Tombstone) { + await this.indexingSvc.tombstoneActor(msg.did) + } +} + +async function getOps( + msg: message.Commit, +): Promise<{ root: Commit; rootCid: CID; ops: PreparedWrite[] }> { + const car = await readCarWithRoot(msg.blocks as Uint8Array) + const rootBytes = car.blocks.get(car.root) + assert(rootBytes, 'Missing commit block in car slice') + + const root = def.commit.schema.parse(cborDecode(rootBytes)) + const ops: PreparedWrite[] = msg.ops.map((op) => { + const [collection, rkey] = op.path.split('/') + assert(collection && rkey) + if ( + op.action === WriteOpAction.Create || + op.action === WriteOpAction.Update + ) { + assert(op.cid) + const record = car.blocks.get(op.cid) + assert(record) + return { + action: + op.action === WriteOpAction.Create + ? WriteOpAction.Create + : WriteOpAction.Update, + cid: op.cid, + record: cborToLexRecord(record), + blobs: [], + uri: AtUri.make(msg.repo, collection, rkey), + } + } else if (op.action === WriteOpAction.Delete) { + return { + action: WriteOpAction.Delete, + uri: AtUri.make(msg.repo, collection, rkey), + } + } else { + throw new Error(`Unknown repo op action: ${op.action}`) + } + }) + + return { root, rootCid: car.root, ops } +} + +function getEnvelope(val: Record): Envelope { + assert(val.repo && val.event, 'malformed message contents') + return { + repo: val.repo.toString(), + event: cborDecode(val.event) as ProcessableMessage, + } +} + +type Envelope = { + repo: string + event: ProcessableMessage +} + +class Partition { + consecutive = new ConsecutiveList() + cursorQueue = new LatestQueue() + constructor(public id: number, public cursor: number) {} + get key() { + return partitionKey(this.id) + } +} + +function partitionId(key: string) { + assert(key.startsWith('repo:')) + return strToInt(key.replace('repo:', '')) +} + +function partitionKey(p: number) { + return `repo:${p}` +} + +type PreparedCreate = { + action: WriteOpAction.Create + uri: AtUri + cid: CID + record: Record + blobs: CID[] // differs from similar type in pds +} + +type PreparedUpdate = { + action: WriteOpAction.Update + uri: AtUri + cid: CID + record: Record + blobs: CID[] // differs from similar type in pds +} + +type PreparedDelete = { + action: WriteOpAction.Delete + uri: AtUri +} + +type PreparedWrite = PreparedCreate | PreparedUpdate | PreparedDelete + +function handleAllSettledErrors(results: PromiseSettledResult[]) { + const errors = results.filter(isRejected).map((res) => res.reason) + if (errors.length === 0) { + return + } + if (errors.length === 1) { + throw errors[0] + } + throw new AggregateError( + errors, + 'Multiple errors: ' + errors.map((err) => err?.message).join('\n'), + ) +} + +function isRejected( + result: PromiseSettledResult, +): result is PromiseRejectedResult { + return result.status === 'rejected' +} diff --git a/packages/bsky/src/ingester/config.ts b/packages/bsky/src/ingester/config.ts new file mode 100644 index 00000000000..49bcf700334 --- /dev/null +++ b/packages/bsky/src/ingester/config.ts @@ -0,0 +1,141 @@ +import assert from 'assert' + +export interface IngesterConfigValues { + version: string + dbPostgresUrl: string + dbPostgresSchema?: string + redisHost?: string // either set redis host, or both sentinel name and hosts + redisSentinelName?: string + redisSentinelHosts?: string[] + redisPassword?: string + repoProvider: string + ingesterPartitionCount: number + ingesterNamespace?: string + ingesterSubLockId?: number + ingesterMaxItems?: number + ingesterCheckItemsEveryN?: number + ingesterInitialCursor?: number +} + +export class IngesterConfig { + constructor(private cfg: IngesterConfigValues) {} + + static readEnv(overrides?: Partial) { + const version = process.env.BSKY_VERSION || '0.0.0' + const dbPostgresUrl = + overrides?.dbPostgresUrl || process.env.DB_POSTGRES_URL + const dbPostgresSchema = + overrides?.dbPostgresSchema || process.env.DB_POSTGRES_SCHEMA + const redisHost = + overrides?.redisHost || process.env.REDIS_HOST || undefined + const redisSentinelName = + overrides?.redisSentinelName || + process.env.REDIS_SENTINEL_NAME || + undefined + const redisSentinelHosts = + overrides?.redisSentinelHosts || + (process.env.REDIS_SENTINEL_HOSTS + ? process.env.REDIS_SENTINEL_HOSTS.split(',') + : []) + const redisPassword = + overrides?.redisPassword || process.env.REDIS_PASSWORD || undefined + const repoProvider = overrides?.repoProvider || process.env.REPO_PROVIDER // E.g. ws://abc.com:4000 + const ingesterPartitionCount = + overrides?.ingesterPartitionCount || + maybeParseInt(process.env.INGESTER_PARTITION_COUNT) + const ingesterSubLockId = + overrides?.ingesterSubLockId || + maybeParseInt(process.env.INGESTER_SUB_LOCK_ID) + const ingesterMaxItems = + overrides?.ingesterMaxItems || + maybeParseInt(process.env.INGESTER_MAX_ITEMS) + const ingesterCheckItemsEveryN = + overrides?.ingesterCheckItemsEveryN || + maybeParseInt(process.env.INGESTER_CHECK_ITEMS_EVERY_N) + const ingesterInitialCursor = + overrides?.ingesterInitialCursor || + maybeParseInt(process.env.INGESTER_INITIAL_CURSOR) + const ingesterNamespace = overrides?.ingesterNamespace + assert(dbPostgresUrl) + assert(redisHost || (redisSentinelName && redisSentinelHosts?.length)) + assert(repoProvider) + assert(ingesterPartitionCount) + return new IngesterConfig({ + version, + dbPostgresUrl, + dbPostgresSchema, + redisHost, + redisSentinelName, + redisSentinelHosts, + redisPassword, + repoProvider, + ingesterPartitionCount, + ingesterSubLockId, + ingesterNamespace, + ingesterMaxItems, + ingesterCheckItemsEveryN, + ingesterInitialCursor, + }) + } + + get version() { + return this.cfg.version + } + + get dbPostgresUrl() { + return this.cfg.dbPostgresUrl + } + + get dbPostgresSchema() { + return this.cfg.dbPostgresSchema + } + + get redisHost() { + return this.cfg.redisHost + } + + get redisSentinelName() { + return this.cfg.redisSentinelName + } + + get redisSentinelHosts() { + return this.cfg.redisSentinelHosts + } + + get redisPassword() { + return this.cfg.redisPassword + } + + get repoProvider() { + return this.cfg.repoProvider + } + + get ingesterPartitionCount() { + return this.cfg.ingesterPartitionCount + } + + get ingesterMaxItems() { + return this.cfg.ingesterMaxItems + } + + get ingesterCheckItemsEveryN() { + return this.cfg.ingesterCheckItemsEveryN + } + + get ingesterInitialCursor() { + return this.cfg.ingesterInitialCursor + } + + get ingesterNamespace() { + return this.cfg.ingesterNamespace + } + + get ingesterSubLockId() { + return this.cfg.ingesterSubLockId + } +} + +function maybeParseInt(str) { + const parsed = parseInt(str) + return isNaN(parsed) ? undefined : parsed +} diff --git a/packages/bsky/src/ingester/context.ts b/packages/bsky/src/ingester/context.ts new file mode 100644 index 00000000000..3dca2bd9762 --- /dev/null +++ b/packages/bsky/src/ingester/context.ts @@ -0,0 +1,27 @@ +import { Database } from '../db' +import { Redis } from '../redis' +import { IngesterConfig } from './config' + +export class IngesterContext { + constructor( + private opts: { + db: Database + redis: Redis + cfg: IngesterConfig + }, + ) {} + + get db(): Database { + return this.opts.db + } + + get redis(): Redis { + return this.opts.redis + } + + get cfg(): IngesterConfig { + return this.opts.cfg + } +} + +export default IngesterContext diff --git a/packages/bsky/src/ingester/index.ts b/packages/bsky/src/ingester/index.ts new file mode 100644 index 00000000000..36096a8be9b --- /dev/null +++ b/packages/bsky/src/ingester/index.ts @@ -0,0 +1,79 @@ +import Database from '../db' +import log from './logger' +import { dbLogger } from '../logger' +import { Redis } from '../redis' +import { IngesterConfig } from './config' +import { IngesterContext } from './context' +import { IngesterSubscription } from './subscription' + +export { IngesterConfig } from './config' +export type { IngesterConfigValues } from './config' + +export class BskyIngester { + public ctx: IngesterContext + public sub: IngesterSubscription + private dbStatsInterval: NodeJS.Timer + private subStatsInterval: NodeJS.Timer + + constructor(opts: { ctx: IngesterContext; sub: IngesterSubscription }) { + this.ctx = opts.ctx + this.sub = opts.sub + } + + static create(opts: { + db: Database + redis: Redis + cfg: IngesterConfig + }): BskyIngester { + const { db, redis, cfg } = opts + const ctx = new IngesterContext({ db, redis, cfg }) + const sub = new IngesterSubscription(ctx, { + service: cfg.repoProvider, + subLockId: cfg.ingesterSubLockId, + partitionCount: cfg.ingesterPartitionCount, + maxItems: cfg.ingesterMaxItems, + checkItemsEveryN: cfg.ingesterCheckItemsEveryN, + initialCursor: cfg.ingesterInitialCursor, + }) + return new BskyIngester({ ctx, sub }) + } + + async start() { + const { db } = this.ctx + const { pool } = db.cfg + this.dbStatsInterval = setInterval(() => { + dbLogger.info( + { + idleCount: pool.idleCount, + totalCount: pool.totalCount, + waitingCount: pool.waitingCount, + }, + 'db pool stats', + ) + }, 10000) + this.subStatsInterval = setInterval(() => { + log.info( + { + seq: this.sub.lastSeq, + streamsLength: + this.sub.backpressure.lastTotal !== null + ? this.sub.backpressure.lastTotal + : undefined, + }, + 'ingester stats', + ) + }, 500) + this.sub.run() + return this + } + + async destroy(opts?: { skipDb: boolean }): Promise { + await this.sub.destroy() + clearInterval(this.subStatsInterval) + await this.ctx.redis.destroy() + if (!opts?.skipDb) await this.ctx.db.close() + clearInterval(this.dbStatsInterval) + } +} + +export default BskyIngester diff --git a/packages/bsky/src/ingester/logger.ts b/packages/bsky/src/ingester/logger.ts new file mode 100644 index 00000000000..0351db66ca9 --- /dev/null +++ b/packages/bsky/src/ingester/logger.ts @@ -0,0 +1,3 @@ +import { subsystemLogger } from '@atproto/common' + +export default subsystemLogger('bsky:ingester') diff --git a/packages/bsky/src/ingester/subscription.ts b/packages/bsky/src/ingester/subscription.ts new file mode 100644 index 00000000000..14f301e07f9 --- /dev/null +++ b/packages/bsky/src/ingester/subscription.ts @@ -0,0 +1,288 @@ +import { + Deferrable, + cborEncode, + createDeferrable, + ui8ToBuffer, + wait, +} from '@atproto/common' +import { randomIntFromSeed } from '@atproto/crypto' +import { DisconnectError, Subscription } from '@atproto/xrpc-server' +import { OutputSchema as Message } from '../lexicon/types/com/atproto/sync/subscribeRepos' +import * as message from '../lexicon/types/com/atproto/sync/subscribeRepos' +import { ids, lexicons } from '../lexicon/lexicons' +import { Leader } from '../db/leader' +import log from './logger' +import { + LatestQueue, + ProcessableMessage, + loggableMessage, + jitter, + strToInt, +} from '../subscription/util' +import { IngesterContext } from './context' + +const METHOD = ids.ComAtprotoSyncSubscribeRepos +const CURSOR_KEY = 'ingester:cursor' +export const INGESTER_SUB_LOCK_ID = 1000 + +export class IngesterSubscription { + cursorQueue = new LatestQueue() + destroyed = false + lastSeq: number | undefined + backpressure = new Backpressure(this) + leader = new Leader(this.opts.subLockId || INGESTER_SUB_LOCK_ID, this.ctx.db) + processor = new Processor(this) + + constructor( + public ctx: IngesterContext, + public opts: { + service: string + partitionCount: number + maxItems?: number + checkItemsEveryN?: number + subLockId?: number + initialCursor?: number + }, + ) {} + + async run() { + while (!this.destroyed) { + try { + const { ran } = await this.leader.run(async ({ signal }) => { + const sub = this.getSubscription({ signal }) + for await (const msg of sub) { + const details = getMessageDetails(msg) + if ('info' in details) { + // These messages are not sequenced, we just log them and carry on + log.warn( + { provider: this.opts.service, message: loggableMessage(msg) }, + `ingester sub ${details.info ? 'info' : 'unknown'} message`, + ) + continue + } + this.processor.send(details) + await this.backpressure.ready() + } + }) + if (ran && !this.destroyed) { + throw new Error('Ingester sub completed, but should be persistent') + } + } catch (err) { + log.error({ err, provider: this.opts.service }, 'ingester sub error') + } + if (!this.destroyed) { + await wait(1000 + jitter(500)) // wait then try to become leader + } + } + } + + async destroy() { + this.destroyed = true + await this.processor.destroy() + await this.cursorQueue.destroy() + this.leader.destroy(new DisconnectError()) + } + + async resume() { + this.destroyed = false + this.processor = new Processor(this) + this.cursorQueue = new LatestQueue() + await this.run() + } + + async getCursor(): Promise { + const val = await this.ctx.redis.get(CURSOR_KEY) + const initialCursor = this.opts.initialCursor ?? 0 + return val !== null ? strToInt(val) : initialCursor + } + + async resetCursor(): Promise { + await this.ctx.redis.del(CURSOR_KEY) + } + + async setCursor(seq: number): Promise { + await this.ctx.redis.set(CURSOR_KEY, seq) + } + + private getSubscription(opts: { signal: AbortSignal }) { + return new Subscription({ + service: this.opts.service, + method: METHOD, + signal: opts.signal, + getParams: async () => { + const cursor = await this.getCursor() + return { cursor } + }, + onReconnectError: (err, reconnects, initial) => { + log.warn({ err, reconnects, initial }, 'ingester sub reconnect') + }, + validate: (value) => { + try { + return lexicons.assertValidXrpcMessage(METHOD, value) + } catch (err) { + log.warn( + { + err, + seq: ifNumber(value?.['seq']), + repo: ifString(value?.['repo']), + commit: ifString(value?.['commit']?.toString()), + time: ifString(value?.['time']), + provider: this.opts.service, + }, + 'ingester sub skipped invalid message', + ) + } + }, + }) + } +} + +function ifString(val: unknown): string | undefined { + return typeof val === 'string' ? val : undefined +} + +function ifNumber(val: unknown): number | undefined { + return typeof val === 'number' ? val : undefined +} + +function getMessageDetails(msg: Message): + | { info: message.Info | null } + | { + seq: number + repo: string + message: ProcessableMessage + } { + if (message.isCommit(msg)) { + return { seq: msg.seq, repo: msg.repo, message: msg } + } else if (message.isHandle(msg)) { + return { seq: msg.seq, repo: msg.did, message: msg } + } else if (message.isMigrate(msg)) { + return { seq: msg.seq, repo: msg.did, message: msg } + } else if (message.isTombstone(msg)) { + return { seq: msg.seq, repo: msg.did, message: msg } + } else if (message.isInfo(msg)) { + return { info: msg } + } + return { info: null } +} + +async function getPartition(did: string, n: number) { + const partition = await randomIntFromSeed(did, n) + return `repo:${partition}` +} + +class Processor { + running: Deferrable | null = null + destroyed = false + unprocessed: MessageEnvelope[] = [] + + constructor(public sub: IngesterSubscription) {} + + async handleBatch(batch: MessageEnvelope[]) { + if (!batch.length) return + const items = await Promise.all( + batch.map(async ({ seq, repo, message }) => { + const key = await getPartition(repo, this.sub.opts.partitionCount) + const fields: [string, string | Buffer][] = [ + ['repo', repo], + ['event', ui8ToBuffer(cborEncode(message))], + ] + return { key, id: seq, fields } + }), + ) + const results = await this.sub.ctx.redis.addMultiToStream(items) + results.forEach(([err], i) => { + if (err) { + // skipping over messages that have already been added or fully processed + const item = batch.at(i) + log.warn( + { seq: item?.seq, repo: item?.repo }, + 'ingester skipping message', + ) + } + }) + const lastSeq = batch[batch.length - 1].seq + this.sub.lastSeq = lastSeq + this.sub.cursorQueue.add(() => this.sub.setCursor(lastSeq)) + } + + async process() { + if (this.running || this.destroyed || !this.unprocessed.length) return + const next = this.unprocessed.splice(100) // pipeline no more than 100 + const processing = this.unprocessed + this.unprocessed = next + this.running = createDeferrable() + try { + await this.handleBatch(processing) + } catch (err) { + log.error( + { err, size: processing.length }, + 'ingester processing failed, rolling over to next batch', + ) + this.unprocessed.unshift(...processing) + } finally { + this.running.resolve() + this.running = null + this.process() + } + } + + send(envelope: MessageEnvelope) { + this.unprocessed.push(envelope) + this.process() + } + + async destroy() { + this.destroyed = true + this.unprocessed = [] + await this.running?.complete + } +} + +type MessageEnvelope = { + seq: number + repo: string + message: ProcessableMessage +} + +class Backpressure { + count = 0 + lastTotal: number | null = null + partitionCount = this.sub.opts.partitionCount + limit = this.sub.opts.maxItems ?? Infinity + checkEvery = this.sub.opts.checkItemsEveryN ?? 500 + + constructor(public sub: IngesterSubscription) {} + + async ready() { + this.count++ + const shouldCheck = + this.limit !== Infinity && + (this.count === 1 || this.count % this.checkEvery === 0) + if (!shouldCheck) return + let ready = false + const start = Date.now() + while (!ready) { + ready = await this.check() + if (!ready) { + log.warn( + { + limit: this.limit, + total: this.lastTotal, + duration: Date.now() - start, + }, + 'ingester backpressure', + ) + await wait(250) + } + } + } + + async check() { + const lens = await this.sub.ctx.redis.streamLengths( + [...Array(this.partitionCount)].map((_, i) => `repo:${i}`), + ) + this.lastTotal = lens.reduce((sum, len) => sum + len, 0) + return this.lastTotal < this.limit + } +} diff --git a/packages/bsky/src/labeler/base.ts b/packages/bsky/src/labeler/base.ts index df601875d03..ddb4f25889e 100644 --- a/packages/bsky/src/labeler/base.ts +++ b/packages/bsky/src/labeler/base.ts @@ -6,8 +6,8 @@ import { labelerLogger as log } from '../logger' import { resolveBlob } from '../api/blob-resolver' import Database from '../db' import { IdResolver } from '@atproto/identity' -import { ServerConfig } from '../config' import { BackgroundQueue } from '../background' +import { IndexerConfig } from '../indexer/config' export abstract class Labeler { public backgroundQueue: BackgroundQueue @@ -15,7 +15,7 @@ export abstract class Labeler { protected ctx: { db: Database idResolver: IdResolver - cfg: ServerConfig + cfg: IndexerConfig backgroundQueue: BackgroundQueue }, ) { diff --git a/packages/bsky/src/labeler/hive.ts b/packages/bsky/src/labeler/hive.ts index 24414fa0468..dfa9ed132bb 100644 --- a/packages/bsky/src/labeler/hive.ts +++ b/packages/bsky/src/labeler/hive.ts @@ -3,10 +3,10 @@ import axios from 'axios' import FormData from 'form-data' import { Labeler } from './base' import { keywordLabeling } from './util' -import { ServerConfig } from '../config' import { IdResolver } from '@atproto/identity' import Database from '../db' import { BackgroundQueue } from '../background' +import { IndexerConfig } from '../indexer/config' const HIVE_ENDPOINT = 'https://api.thehive.ai/api/v2/task/sync' @@ -19,7 +19,7 @@ export class HiveLabeler extends Labeler { protected ctx: { db: Database idResolver: IdResolver - cfg: ServerConfig + cfg: IndexerConfig backgroundQueue: BackgroundQueue }, ) { diff --git a/packages/bsky/src/labeler/keyword.ts b/packages/bsky/src/labeler/keyword.ts index b3fd745eaf1..ddd3dfecda8 100644 --- a/packages/bsky/src/labeler/keyword.ts +++ b/packages/bsky/src/labeler/keyword.ts @@ -2,9 +2,9 @@ import Database from '../db' import { Labeler } from './base' import { getFieldsFromRecord, keywordLabeling } from './util' import { IdResolver } from '@atproto/identity' -import { ServerConfig } from '../config' import { BackgroundQueue } from '../background' import { AtUri } from '@atproto/uri' +import { IndexerConfig } from '../indexer/config' export class KeywordLabeler extends Labeler { keywords: Record @@ -13,7 +13,7 @@ export class KeywordLabeler extends Labeler { protected ctx: { db: Database idResolver: IdResolver - cfg: ServerConfig + cfg: IndexerConfig backgroundQueue: BackgroundQueue }, ) { diff --git a/packages/bsky/src/redis.ts b/packages/bsky/src/redis.ts new file mode 100644 index 00000000000..2df5ff73768 --- /dev/null +++ b/packages/bsky/src/redis.ts @@ -0,0 +1,149 @@ +import assert from 'assert' +import { Redis as RedisDriver } from 'ioredis' + +export class Redis { + driver: RedisDriver + namespace?: string + constructor(opts: RedisOptions) { + if ('sentinel' in opts) { + assert(opts.sentinel && Array.isArray(opts.hosts) && opts.hosts.length) + this.driver = new RedisDriver({ + name: opts.sentinel, + sentinels: opts.hosts.map((h) => addressParts(h, 26379)), + password: opts.password, + }) + } else if ('host' in opts) { + assert(opts.host) + this.driver = new RedisDriver({ + ...addressParts(opts.host), + password: opts.password, + }) + } else { + assert(opts.driver) + this.driver = opts.driver + } + this.namespace = opts.namespace + } + + async readStreams( + streams: StreamRef[], + opts: { count: number; blockMs?: number }, + ) { + const allRead = await this.driver.xreadBuffer( + 'COUNT', + opts.count, // events per stream + 'BLOCK', + opts.blockMs ?? 1000, // millis + 'STREAMS', + ...streams.map((s) => this.ns(s.key)), + ...streams.map((s) => s.cursor), + ) + const results: StreamOutput[] = [] + for (const [key, messages] of allRead ?? []) { + const result: StreamOutput = { + key: this.rmns(key.toString()), + messages: [], + } + results.push(result) + for (const [seqBuf, values] of messages) { + const message = { cursor: seqBuf.toString(), contents: {} } + result.messages.push(message) + for (let i = 0; i < values.length; ++i) { + if (i % 2 === 0) continue + const field = values[i - 1].toString() + message.contents[field] = values[i] + } + } + } + return results + } + + async addToStream( + key: string, + id: number | string, + fields: [key: string, value: string | Buffer][], + ) { + await this.driver.xadd(this.ns(key), id, ...fields.flat()) + } + + async addMultiToStream( + evts: { + key: string + id: number | string + fields: [key: string, value: string | Buffer][] + }[], + ) { + const pipeline = this.driver.pipeline() + for (const { key, id, fields } of evts) { + pipeline.xadd(this.ns(key), id, ...fields.flat()) + } + return (await pipeline.exec()) ?? [] + } + + async trimStream(key: string, cursor: number | string) { + await this.driver.xtrim(this.ns(key), 'MINID', cursor) + } + + async streamLengths(keys: string[]) { + const pipeline = this.driver.pipeline() + for (const key of keys) { + pipeline.xlen(this.ns(key)) + } + const results = await pipeline.exec() + return (results ?? []).map(([, len = 0]) => Number(len)) + } + + async get(key: string) { + return await this.driver.get(this.ns(key)) + } + + async set(key: string, val: string | number) { + await this.driver.set(this.ns(key), val) + } + + async del(key: string) { + return await this.driver.del(this.ns(key)) + } + + async destroy() { + await this.driver.quit() + } + + // namespace redis keys + ns(key: string) { + return this.namespace ? `${this.namespace}:${key}` : key + } + + // remove namespace from redis key + rmns(key: string) { + return this.namespace && key.startsWith(`${this.namespace}:`) + ? key.replace(`${this.namespace}:`, '') + : key + } +} + +type StreamRef = { key: string; cursor: string | number } + +type StreamOutput = { + key: string + messages: { cursor: string; contents: Record }[] +} + +export type RedisOptions = ( + | { driver: RedisDriver } + | { host: string } + | { sentinel: string; hosts: string[] } +) & { + password?: string + namespace?: string +} + +function addressParts( + addr: string, + defaultPort = 6379, +): { host: string; port: number } { + const [host, portStr, ...others] = addr.split(':') + const port = portStr ? parseInt(portStr, 10) : defaultPort + assert(host && !isNaN(port) && !others.length, `invalid address: ${addr}`) + return { host, port } +} diff --git a/packages/bsky/src/services/index.ts b/packages/bsky/src/services/index.ts index a17c71c6d13..f8863986bbd 100644 --- a/packages/bsky/src/services/index.ts +++ b/packages/bsky/src/services/index.ts @@ -1,35 +1,21 @@ -import { IdResolver } from '@atproto/identity' import Database from '../db' import { ImageUriBuilder } from '../image/uri' import { ActorService } from './actor' import { FeedService } from './feed' import { GraphService } from './graph' -import { IndexingService } from './indexing' import { ModerationService } from './moderation' import { LabelService } from './label' import { ImageInvalidator } from '../image/invalidator' -import { Labeler } from '../labeler' -import { BackgroundQueue } from '../background' export function createServices(resources: { imgUriBuilder: ImageUriBuilder imgInvalidator: ImageInvalidator - idResolver: IdResolver - labeler: Labeler - backgroundQueue: BackgroundQueue }): Services { - const { - imgUriBuilder, - imgInvalidator, - idResolver, - labeler, - backgroundQueue, - } = resources + const { imgUriBuilder, imgInvalidator } = resources return { actor: ActorService.creator(imgUriBuilder), feed: FeedService.creator(imgUriBuilder), graph: GraphService.creator(imgUriBuilder), - indexing: IndexingService.creator(idResolver, labeler, backgroundQueue), moderation: ModerationService.creator(imgUriBuilder, imgInvalidator), label: LabelService.creator(), } @@ -39,7 +25,6 @@ export type Services = { actor: FromDb feed: FromDb graph: FromDb - indexing: FromDb moderation: FromDb label: FromDb } diff --git a/packages/bsky/src/subscription/repo.ts b/packages/bsky/src/subscription/repo.ts deleted file mode 100644 index 8a0a13ed135..00000000000 --- a/packages/bsky/src/subscription/repo.ts +++ /dev/null @@ -1,457 +0,0 @@ -import assert from 'node:assert' -import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' -import { cborDecode, wait } from '@atproto/common' -import { DisconnectError, Subscription } from '@atproto/xrpc-server' -import { - WriteOpAction, - readCarWithRoot, - cborToLexRecord, - def, - Commit, -} from '@atproto/repo' -import { ValidationError } from '@atproto/lexicon' -import { OutputSchema as Message } from '../lexicon/types/com/atproto/sync/subscribeRepos' -import * as message from '../lexicon/types/com/atproto/sync/subscribeRepos' -import { ids, lexicons } from '../lexicon/lexicons' -import Database from '../db' -import AppContext from '../context' -import { Leader } from '../db/leader' -import { IndexingService } from '../services/indexing' -import { subLogger } from '../logger' -import { - ConsecutiveItem, - ConsecutiveList, - LatestQueue, - PartitionedQueue, -} from './util' - -const METHOD = ids.ComAtprotoSyncSubscribeRepos -export const REPO_SUB_ID = 1000 - -export class RepoSubscription { - leader = new Leader(this.subLockId, this.ctx.db) - repoQueue: PartitionedQueue - cursorQueue = new LatestQueue() - consecutive = new ConsecutiveList() - destroyed = false - lastSeq: number | undefined - lastCursor: number | undefined - indexingSvc: IndexingService - - constructor( - public ctx: AppContext, - public service: string, - public subLockId = REPO_SUB_ID, - public concurrency = Infinity, - ) { - this.repoQueue = new PartitionedQueue({ concurrency }) - this.indexingSvc = ctx.services.indexing(ctx.db) - } - - async run() { - while (!this.destroyed) { - try { - const { ran } = await this.leader.run(async ({ signal }) => { - const sub = this.getSubscription({ signal }) - for await (const msg of sub) { - const details = getMessageDetails(msg) - if ('info' in details) { - // These messages are not sequenced, we just log them and carry on - subLogger.warn( - { provider: this.service, message: loggableMessage(msg) }, - `repo subscription ${ - details.info ? 'info' : 'unknown' - } message`, - ) - continue - } - this.lastSeq = details.seq - const item = this.consecutive.push(details.seq) - this.repoQueue.add(details.repo, () => - this.handleMessage(item, details.message), - ) - await this.repoQueue.main.onEmpty() // backpressure - } - }) - if (ran && !this.destroyed) { - throw new Error('Repo sub completed, but should be persistent') - } - } catch (err) { - subLogger.error( - { err, provider: this.service }, - 'repo subscription error', - ) - } - if (!this.destroyed) { - await wait(5000 + jitter(1000)) // wait then try to become leader - } - } - } - - async destroy() { - this.destroyed = true - await this.repoQueue.destroy() - await this.cursorQueue.destroy() - this.leader.destroy(new DisconnectError()) - } - - async resume() { - this.destroyed = false - this.repoQueue = new PartitionedQueue({ concurrency: this.concurrency }) - this.cursorQueue = new LatestQueue() - this.consecutive = new ConsecutiveList() - await this.run() - } - - private async handleMessage( - item: ConsecutiveItem, - msg: ProcessableMessage, - ) { - try { - if (message.isCommit(msg)) { - await this.handleCommit(msg) - } else if (message.isHandle(msg)) { - await this.handleUpdateHandle(msg) - } else if (message.isTombstone(msg)) { - await this.handleTombstone(msg) - } else if (message.isMigrate(msg)) { - // Ignore migrations - } else { - const exhaustiveCheck: never = msg - throw new Error(`Unhandled message type: ${exhaustiveCheck['$type']}`) - } - } catch (err) { - // We log messages we can't process and move on. Barring a - // durable queue this is the best we can do for now: otherwise - // the cursor would get stuck on a poison message. - subLogger.error( - { - err, - provider: this.service, - message: loggableMessage(msg), - }, - 'repo subscription message processing error', - ) - } finally { - const latest = item.complete().at(-1) - if (latest) { - this.cursorQueue - .add(() => this.handleCursor(latest)) - .catch((err) => { - subLogger.error( - { err, provider: this.service }, - 'repo subscription cursor error', - ) - }) - } - } - } - - private async handleCommit(msg: message.Commit) { - const indexRecords = async () => { - const { root, rootCid, ops } = await getOps(msg) - if (msg.tooBig) { - await this.indexingSvc.indexRepo(msg.repo, rootCid.toString()) - await this.indexingSvc.setCommitLastSeen(root, msg) - return - } - if (msg.rebase) { - const needsReindex = await this.indexingSvc.checkCommitNeedsIndexing( - root, - ) - if (needsReindex) { - await this.indexingSvc.indexRepo(msg.repo, rootCid.toString()) - } - await this.indexingSvc.setCommitLastSeen(root, msg) - return - } - for (const op of ops) { - if (op.action === WriteOpAction.Delete) { - await this.indexingSvc.deleteRecord(op.uri) - } else { - try { - await this.indexingSvc.indexRecord( - op.uri, - op.cid, - op.record, - op.action, // create or update - msg.time, - ) - } catch (err) { - if (err instanceof ValidationError) { - subLogger.warn( - { - did: msg.repo, - commit: msg.commit.toString(), - uri: op.uri.toString(), - cid: op.cid.toString(), - }, - 'skipping indexing of invalid record', - ) - } else { - subLogger.error( - { - err, - did: msg.repo, - commit: msg.commit.toString(), - uri: op.uri.toString(), - cid: op.cid.toString(), - }, - 'skipping indexing due to error processing record', - ) - } - } - } - } - await this.indexingSvc.setCommitLastSeen(root, msg) - } - const results = await Promise.allSettled([ - indexRecords(), - this.indexingSvc.indexHandle(msg.repo, msg.time), - ]) - handleAllSettledErrors(results) - } - - private async handleUpdateHandle(msg: message.Handle) { - await this.indexingSvc.indexHandle(msg.did, msg.time, true) - } - - private async handleTombstone(msg: message.Tombstone) { - await this.indexingSvc.tombstoneActor(msg.did) - } - - private async handleCursor(seq: number) { - const { db } = this.ctx - await db.transaction(async (tx) => { - await this.setState(tx, { cursor: seq }) - }) - } - - async getState(): Promise { - const sub = await this.ctx.db.db - .selectFrom('subscription') - .selectAll() - .where('service', '=', this.service) - .where('method', '=', METHOD) - .executeTakeFirst() - const state = sub ? (JSON.parse(sub.state) as State) : { cursor: 0 } - this.lastCursor = state.cursor - return state - } - - async resetState(): Promise { - await this.ctx.db.db - .deleteFrom('subscription') - .where('service', '=', this.service) - .where('method', '=', METHOD) - .executeTakeFirst() - } - - private async setState(tx: Database, state: State): Promise { - tx.assertTransaction() - tx.onCommit(() => { - this.lastCursor = state.cursor - }) - const res = await tx.db - .updateTable('subscription') - .where('service', '=', this.service) - .where('method', '=', METHOD) - .set({ state: JSON.stringify(state) }) - .executeTakeFirst() - if (res.numUpdatedRows < 1) { - await tx.db - .insertInto('subscription') - .values({ - service: this.service, - method: METHOD, - state: JSON.stringify(state), - }) - .executeTakeFirst() - } - } - - private getSubscription(opts: { signal: AbortSignal }) { - return new Subscription({ - service: this.service, - method: METHOD, - signal: opts.signal, - getParams: () => this.getState(), - onReconnectError: (err, reconnects, initial) => { - subLogger.warn( - { err, reconnects, initial }, - 'repo subscription reconnect', - ) - }, - validate: (value) => { - try { - return lexicons.assertValidXrpcMessage(METHOD, value) - } catch (err) { - subLogger.warn( - { - err, - seq: ifNumber(value?.['seq']), - repo: ifString(value?.['repo']), - commit: ifString(value?.['commit']?.toString()), - time: ifString(value?.['time']), - provider: this.service, - }, - 'repo subscription skipped invalid message', - ) - } - }, - }) - } -} - -// These are the message types that have a sequence number and a repo -type ProcessableMessage = - | message.Commit - | message.Handle - | message.Migrate - | message.Tombstone - -async function getOps( - msg: message.Commit, -): Promise<{ root: Commit; rootCid: CID; ops: PreparedWrite[] }> { - const car = await readCarWithRoot(msg.blocks as Uint8Array) - const rootBytes = car.blocks.get(car.root) - assert(rootBytes, 'Missing commit block in car slice') - - const root = def.commit.schema.parse(cborDecode(rootBytes)) - const ops: PreparedWrite[] = msg.ops.map((op) => { - const [collection, rkey] = op.path.split('/') - assert(collection && rkey) - if ( - op.action === WriteOpAction.Create || - op.action === WriteOpAction.Update - ) { - assert(op.cid) - const record = car.blocks.get(op.cid) - assert(record) - return { - action: - op.action === WriteOpAction.Create - ? WriteOpAction.Create - : WriteOpAction.Update, - cid: op.cid, - record: cborToLexRecord(record), - blobs: [], - uri: AtUri.make(msg.repo, collection, rkey), - } - } else if (op.action === WriteOpAction.Delete) { - return { - action: WriteOpAction.Delete, - uri: AtUri.make(msg.repo, collection, rkey), - } - } else { - throw new Error(`Unknown repo op action: ${op.action}`) - } - }) - - return { root, rootCid: car.root, ops } -} - -function jitter(maxMs) { - return Math.round((Math.random() - 0.5) * maxMs * 2) -} - -function ifString(val: unknown): string | undefined { - return typeof val === 'string' ? val : undefined -} - -function ifNumber(val: unknown): number | undefined { - return typeof val === 'number' ? val : undefined -} - -function loggableMessage(msg: Message) { - if (message.isCommit(msg)) { - const { seq, rebase, prev, repo, commit, time, tooBig, blobs } = msg - return { - $type: msg.$type, - seq, - rebase, - prev: prev?.toString(), - repo, - commit: commit.toString(), - time, - tooBig, - hasBlobs: blobs.length > 0, - } - } else if (message.isHandle(msg)) { - return msg - } else if (message.isMigrate(msg)) { - return msg - } else if (message.isTombstone(msg)) { - return msg - } else if (message.isInfo(msg)) { - return msg - } - return msg -} - -type State = { cursor: number } - -type PreparedCreate = { - action: WriteOpAction.Create - uri: AtUri - cid: CID - record: Record - blobs: CID[] // differs from similar type in pds -} - -type PreparedUpdate = { - action: WriteOpAction.Update - uri: AtUri - cid: CID - record: Record - blobs: CID[] // differs from similar type in pds -} - -type PreparedDelete = { - action: WriteOpAction.Delete - uri: AtUri -} - -type PreparedWrite = PreparedCreate | PreparedUpdate | PreparedDelete - -function getMessageDetails(msg: Message): - | { info: message.Info | null } - | { - seq: number - repo: string - message: ProcessableMessage - } { - if (message.isCommit(msg)) { - return { seq: msg.seq, repo: msg.repo, message: msg } - } else if (message.isHandle(msg)) { - return { seq: msg.seq, repo: msg.did, message: msg } - } else if (message.isMigrate(msg)) { - return { seq: msg.seq, repo: msg.did, message: msg } - } else if (message.isTombstone(msg)) { - return { seq: msg.seq, repo: msg.did, message: msg } - } else if (message.isInfo(msg)) { - return { info: msg } - } - return { info: null } -} - -function handleAllSettledErrors(results: PromiseSettledResult[]) { - const errors = results.filter(isRejected).map((res) => res.reason) - if (errors.length === 0) { - return - } - if (errors.length === 1) { - throw errors[0] - } - throw new AggregateError( - errors, - 'Multiple errors: ' + errors.map((err) => err?.message).join('\n'), - ) -} - -function isRejected( - result: PromiseSettledResult, -): result is PromiseRejectedResult { - return result.status === 'rejected' -} diff --git a/packages/bsky/src/subscription/util.ts b/packages/bsky/src/subscription/util.ts index 0bd6b862176..fe367bcc24c 100644 --- a/packages/bsky/src/subscription/util.ts +++ b/packages/bsky/src/subscription/util.ts @@ -1,4 +1,7 @@ import PQueue from 'p-queue' +import { OutputSchema as RepoMessage } from '../lexicon/types/com/atproto/sync/subscribeRepos' +import * as message from '../lexicon/types/com/atproto/sync/subscribeRepos' +import assert from 'node:assert' // A queue with arbitrarily many partitions, each processing work sequentially. // Partitions are created lazily and taken out of memory when they go idle. @@ -92,3 +95,54 @@ export class ConsecutiveItem { return this.consecutive.complete() } } + +export class PerfectMap extends Map { + get(key: K): V { + const val = super.get(key) + assert(val !== undefined, `Key not found in PerfectMap: ${key}`) + return val + } +} + +// These are the message types that have a sequence number and a repo +export type ProcessableMessage = + | message.Commit + | message.Handle + | message.Migrate + | message.Tombstone + +export function loggableMessage(msg: RepoMessage) { + if (message.isCommit(msg)) { + const { seq, rebase, prev, repo, commit, time, tooBig, blobs } = msg + return { + $type: msg.$type, + seq, + rebase, + prev: prev?.toString(), + repo, + commit: commit.toString(), + time, + tooBig, + hasBlobs: blobs.length > 0, + } + } else if (message.isHandle(msg)) { + return msg + } else if (message.isMigrate(msg)) { + return msg + } else if (message.isTombstone(msg)) { + return msg + } else if (message.isInfo(msg)) { + return msg + } + return msg +} + +export function jitter(maxMs) { + return Math.round((Math.random() - 0.5) * maxMs * 2) +} + +export function strToInt(str: string) { + const int = parseInt(str, 10) + assert(!isNaN(int), 'string could not be parsed to an integer') + return int +} diff --git a/packages/bsky/tests/algos/hot-classic.test.ts b/packages/bsky/tests/algos/hot-classic.test.ts index 1383bfe325c..d7f3c221e1b 100644 --- a/packages/bsky/tests/algos/hot-classic.test.ts +++ b/packages/bsky/tests/algos/hot-classic.test.ts @@ -33,7 +33,7 @@ describe('algo hot-classic', () => { alice = sc.dids.alice bob = sc.dids.bob await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() }) afterAll(async () => { @@ -61,7 +61,7 @@ describe('algo hot-classic', () => { await sc.like(sc.dids[name], two.ref) await sc.like(sc.dids[name], three.ref) } - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() const res = await agent.api.app.bsky.feed.getFeed( { feed: feedUri }, diff --git a/packages/bsky/tests/algos/whats-hot.test.ts b/packages/bsky/tests/algos/whats-hot.test.ts index 4a214bf76ab..dc26f51528b 100644 --- a/packages/bsky/tests/algos/whats-hot.test.ts +++ b/packages/bsky/tests/algos/whats-hot.test.ts @@ -36,7 +36,7 @@ describe.skip('algo whats-hot', () => { bob = sc.dids.bob carol = sc.dids.carol await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() }) afterAll(async () => { @@ -74,7 +74,7 @@ describe.skip('algo whats-hot', () => { await sc.like(sc.dids[name], five.ref) } } - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() // move the 3rd post 5 hours into the past to check gravity await network.bsky.ctx.db.db diff --git a/packages/bsky/tests/algos/with-friends.test.ts b/packages/bsky/tests/algos/with-friends.test.ts index c0927486e18..12f35083ae4 100644 --- a/packages/bsky/tests/algos/with-friends.test.ts +++ b/packages/bsky/tests/algos/with-friends.test.ts @@ -37,7 +37,7 @@ describe.skip('algo with friends', () => { carol = sc.dids.carol dan = sc.dids.dan await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() }) afterAll(async () => { @@ -109,7 +109,7 @@ describe.skip('algo with friends', () => { await sc.like(carol, nine.ref) await sc.like(carol, ten.ref) - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() expectedFeed = [ ten.ref.uriStr, diff --git a/packages/bsky/tests/blob-resolver.test.ts b/packages/bsky/tests/blob-resolver.test.ts index 3f067ac66fb..fcb2b657ee5 100644 --- a/packages/bsky/tests/blob-resolver.test.ts +++ b/packages/bsky/tests/blob-resolver.test.ts @@ -20,7 +20,7 @@ describe('blob resolver', () => { const sc = new SeedClient(pdsAgent) await basicSeed(sc) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() fileDid = sc.dids.carol fileCid = sc.posts[fileDid][0].images[0].image.ref client = axios.create({ diff --git a/packages/bsky/tests/did-cache.test.ts b/packages/bsky/tests/did-cache.test.ts index 9c8a1ec1602..659c6eb3bcf 100644 --- a/packages/bsky/tests/did-cache.test.ts +++ b/packages/bsky/tests/did-cache.test.ts @@ -21,8 +21,8 @@ describe('did cache', () => { network = await TestNetwork.create({ dbPostgresSchema: 'bsky_did_cache', }) - idResolver = network.bsky.ctx.idResolver - didCache = network.bsky.ctx.didCache + idResolver = network.bsky.indexer.ctx.idResolver + didCache = network.bsky.indexer.ctx.didCache const pdsAgent = new AtpAgent({ service: network.pds.url }) sc = new SeedClient(pdsAgent) await userSeed(sc) diff --git a/packages/bsky/tests/duplicate-records.test.ts b/packages/bsky/tests/duplicate-records.test.ts index d02014d99a7..ec66b7d30dc 100644 --- a/packages/bsky/tests/duplicate-records.test.ts +++ b/packages/bsky/tests/duplicate-records.test.ts @@ -4,7 +4,7 @@ import { WriteOpAction } from '@atproto/repo' import { TestNetwork } from '@atproto/dev-env' import { Database } from '../src' import * as lex from '../src/lexicon/lexicons' -import { Services } from '../src/services' +import { Services } from '../src/indexer/services' describe('duplicate record', () => { let network: TestNetwork @@ -16,8 +16,8 @@ describe('duplicate record', () => { network = await TestNetwork.create({ dbPostgresSchema: 'bsky_duplicates', }) - db = network.bsky.ctx.db - services = network.bsky.ctx.services + db = network.bsky.indexer.ctx.db + services = network.bsky.indexer.ctx.services did = 'did:example:alice' }) diff --git a/packages/bsky/tests/feed-generation.test.ts b/packages/bsky/tests/feed-generation.test.ts index 950203bc877..70eb7fe82dd 100644 --- a/packages/bsky/tests/feed-generation.test.ts +++ b/packages/bsky/tests/feed-generation.test.ts @@ -41,7 +41,6 @@ describe('feed generation', () => { sc = new SeedClient(pdsAgent) await basicSeed(sc) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() alice = sc.dids.alice const allUri = AtUri.make(alice, 'app.bsky.feed.generator', 'all') const feedUriBadPagination = AtUri.make( @@ -166,7 +165,6 @@ describe('feed generation', () => { { headers: sc.getHeaders(alice), encoding: 'application/json' }, ) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() }) it('getActorFeeds fetches feed generators by actor.', async () => { @@ -174,7 +172,6 @@ describe('feed generation', () => { await sc.like(sc.dids.bob, feedUriAllRef) await sc.like(sc.dids.carol, feedUriAllRef) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() const results = (results) => results.flatMap((res) => res.feeds) const paginator = async (cursor?: string) => { @@ -210,7 +207,6 @@ describe('feed generation', () => { sc.getHeaders(sc.dids.bob), ) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() const view = await agent.api.app.bsky.feed.getPosts( { uris: [res.uri] }, { headers: await network.serviceHeaders(sc.dids.bob) }, @@ -293,7 +289,7 @@ describe('feed generation', () => { sc.getHeaders(sc.dids.bob), ) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() // now take it offline await bobFg.close() diff --git a/packages/bsky/tests/handle-invalidation.test.ts b/packages/bsky/tests/handle-invalidation.test.ts index 9918ee08826..b2387b739ff 100644 --- a/packages/bsky/tests/handle-invalidation.test.ts +++ b/packages/bsky/tests/handle-invalidation.test.ts @@ -27,8 +27,10 @@ describe('handle invalidation', () => { alice = sc.dids.alice bob = sc.dids.bob - const origResolve = network.bsky.ctx.idResolver.handle.resolve - network.bsky.ctx.idResolver.handle.resolve = async (handle: string) => { + const origResolve = network.bsky.indexer.ctx.idResolver.handle.resolve + network.bsky.indexer.ctx.idResolver.handle.resolve = async ( + handle: string, + ) => { if (mockHandles[handle] === null) { return undefined } else if (mockHandles[handle]) { diff --git a/packages/bsky/tests/image/server.test.ts b/packages/bsky/tests/image/server.test.ts index ff887a28920..b92f3c77e7c 100644 --- a/packages/bsky/tests/image/server.test.ts +++ b/packages/bsky/tests/image/server.test.ts @@ -26,7 +26,7 @@ describe('image processing server', () => { const sc = new SeedClient(pdsAgent) await basicSeed(sc) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() fileDid = sc.dids.carol fileCid = sc.posts[fileDid][0].images[0].image.ref client = axios.create({ diff --git a/packages/bsky/tests/indexing.test.ts b/packages/bsky/tests/indexing.test.ts index 180b7b643e6..bb0f9129433 100644 --- a/packages/bsky/tests/indexing.test.ts +++ b/packages/bsky/tests/indexing.test.ts @@ -36,8 +36,9 @@ describe('indexing', () => { await usersSeed(sc) // Data in tests is not processed from subscription await network.processAll() - await network.bsky.sub.destroy() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.ingester.sub.destroy() + await network.bsky.indexer.sub.destroy() + await network.bsky.processAll() }) afterAll(async () => { @@ -45,7 +46,7 @@ describe('indexing', () => { }) it('indexes posts.', async () => { - const { db, services } = network.bsky.ctx + const { db, services } = network.bsky.indexer.ctx const createdAt = new Date().toISOString() const createRecord = await prepareCreate({ did: sc.dids.alice, @@ -135,7 +136,7 @@ describe('indexing', () => { }) it('indexes profiles.', async () => { - const { db, services } = network.bsky.ctx + const { db, services } = network.bsky.indexer.ctx const createRecord = await prepareCreate({ did: sc.dids.dan, collection: ids.AppBskyActorProfile, @@ -190,7 +191,7 @@ describe('indexing', () => { }) it('handles post aggregations out of order.', async () => { - const { db, services } = network.bsky.ctx + const { db, services } = network.bsky.indexer.ctx const createdAt = new Date().toISOString() const originalPost = await prepareCreate({ did: sc.dids.alice, @@ -241,7 +242,7 @@ describe('indexing', () => { await services.indexing(db).indexRecord(...like) await services.indexing(db).indexRecord(...repost) await services.indexing(db).indexRecord(...originalPost) - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() const agg = await db.db .selectFrom('post_agg') .selectAll() @@ -268,7 +269,7 @@ describe('indexing', () => { }) it('handles profile aggregations out of order.', async () => { - const { db, services } = network.bsky.ctx + const { db, services } = network.bsky.indexer.ctx const createdAt = new Date().toISOString() const unknownDid = 'did:example:unknown' const follow = await prepareCreate({ @@ -281,7 +282,7 @@ describe('indexing', () => { } as AppBskyGraphFollow.Record, }) await services.indexing(db).indexRecord(...follow) - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() const agg = await db.db .selectFrom('profile_agg') .select(['did', 'followersCount']) @@ -304,15 +305,17 @@ describe('indexing', () => { describe('indexRepo', () => { beforeAll(async () => { - network.bsky.sub.resume() + network.bsky.indexer.sub.resume() + network.bsky.ingester.sub.resume() await basicSeed(sc, false) await network.processAll() - await network.bsky.sub.destroy() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.ingester.sub.destroy() + await network.bsky.indexer.sub.destroy() + await network.bsky.processAll() }) it('preserves indexes when no record changes.', async () => { - const { db, services } = network.bsky.ctx + const { db, services } = network.bsky.indexer.ctx // Mark originals const { data: origProfile } = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, @@ -331,6 +334,7 @@ describe('indexing', () => { did: sc.dids.alice, }) await services.indexing(db).indexRepo(sc.dids.alice, head.root) + await network.bsky.processAll() // Check const { data: profile } = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, @@ -350,7 +354,7 @@ describe('indexing', () => { }) it('updates indexes when records change.', async () => { - const { db, services } = network.bsky.ctx + const { db, services } = network.bsky.indexer.ctx // Update profile await pdsAgent.api.com.atproto.repo.putRecord( { @@ -374,6 +378,7 @@ describe('indexing', () => { did: sc.dids.alice, }) await services.indexing(db).indexRepo(sc.dids.alice, head.root) + await network.bsky.processAll() // Check const { data: profile } = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, @@ -395,7 +400,7 @@ describe('indexing', () => { }) it('skips invalid records.', async () => { - const { db, services } = network.bsky.ctx + const { db, services } = network.bsky.indexer.ctx const { db: pdsDb, services: pdsServices } = network.pds.ctx // Create a good and a bad post record const writes = await Promise.all([ @@ -443,7 +448,7 @@ describe('indexing', () => { } it('indexes handle for a fresh did', async () => { - const { db, services } = network.bsky.ctx + const { db, services } = network.bsky.indexer.ctx const now = new Date().toISOString() const sessionAgent = new AtpAgent({ service: network.pds.url }) const { @@ -459,7 +464,7 @@ describe('indexing', () => { }) it('reindexes handle for existing did when forced', async () => { - const { db, services } = network.bsky.ctx + const { db, services } = network.bsky.indexer.ctx const now = new Date().toISOString() const sessionAgent = new AtpAgent({ service: network.pds.url }) const { @@ -481,7 +486,7 @@ describe('indexing', () => { }) it('handles profile aggregations out of order', async () => { - const { db, services } = network.bsky.ctx + const { db, services } = network.bsky.indexer.ctx const now = new Date().toISOString() const sessionAgent = new AtpAgent({ service: network.pds.url }) const { @@ -502,7 +507,7 @@ describe('indexing', () => { }) await services.indexing(db).indexRecord(...follow) await services.indexing(db).indexHandle(did, now) - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() const agg = await db.db .selectFrom('profile_agg') .select(['did', 'followersCount']) @@ -517,7 +522,7 @@ describe('indexing', () => { describe('tombstoneActor', () => { it('does not unindex actor when they are still being hosted by their pds', async () => { - const { db, services } = network.bsky.ctx + const { db, services } = network.bsky.indexer.ctx const { data: profileBefore } = await agent.api.app.bsky.actor.getProfile( { actor: sc.dids.alice }, { headers: await network.serviceHeaders(sc.dids.bob) }, @@ -532,7 +537,7 @@ describe('indexing', () => { }) it('unindexes actor when they are no longer hosted by their pds', async () => { - const { db, services } = network.bsky.ctx + const { db, services } = network.bsky.indexer.ctx const { alice } = sc.dids const getProfileBefore = agent.api.app.bsky.actor.getProfile( { actor: alice }, diff --git a/packages/bsky/tests/labeler/labeler.test.ts b/packages/bsky/tests/labeler/labeler.test.ts index 7521349deaf..58d11765987 100644 --- a/packages/bsky/tests/labeler/labeler.test.ts +++ b/packages/bsky/tests/labeler/labeler.test.ts @@ -1,7 +1,8 @@ import { AtUri, AtpAgent, BlobRef } from '@atproto/api' import stream, { Readable } from 'stream' import { Labeler } from '../../src/labeler' -import { AppContext, Database, ServerConfig } from '../../src' +import { Database, IndexerConfig } from '../../src' +import IndexerContext from '../../src/indexer/context' import { cidForRecord } from '@atproto/repo' import { keywordLabeling } from '../../src/labeler/util' import { cidForCbor, streamToBytes, TID } from '@atproto/common' @@ -17,23 +18,22 @@ describe('labeler', () => { let network: TestNetwork let labeler: Labeler let labelSrvc: LabelService - let ctx: AppContext + let ctx: IndexerContext let labelerDid: string let badBlob1: BlobRef let badBlob2: BlobRef let goodBlob: BlobRef let alice: string const postUri = () => AtUri.make(alice, 'app.bsky.feed.post', TID.nextStr()) - const profileUri = () => AtUri.make(alice, 'app.bsky.actor.profile', 'self') beforeAll(async () => { network = await TestNetwork.create({ dbPostgresSchema: 'bsky_labeler', }) - ctx = network.bsky.ctx + ctx = network.bsky.indexer.ctx const pdsCtx = network.pds.ctx labelerDid = ctx.cfg.labelerDid - labeler = new TestLabeler(ctx) + labeler = new TestLabeler(network.bsky.indexer.ctx) labelSrvc = ctx.services.label(ctx.db) const pdsAgent = new AtpAgent({ service: network.pds.url }) const sc = new SeedClient(pdsAgent) @@ -159,7 +159,7 @@ class TestLabeler extends Labeler { constructor(opts: { db: Database idResolver: IdResolver - cfg: ServerConfig + cfg: IndexerConfig backgroundQueue: BackgroundQueue }) { const { db, cfg, idResolver, backgroundQueue } = opts diff --git a/packages/bsky/tests/pipeline/backpressure.test.ts b/packages/bsky/tests/pipeline/backpressure.test.ts new file mode 100644 index 00000000000..a265bc948c5 --- /dev/null +++ b/packages/bsky/tests/pipeline/backpressure.test.ts @@ -0,0 +1,69 @@ +import { wait } from '@atproto/common' +import { + BskyIndexers, + TestNetworkNoAppView, + getIndexers, + getIngester, + processAll, +} from '@atproto/dev-env' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' +import { BskyIngester } from '../../src' + +const TEST_NAME = 'pipeline_backpressure' + +describe('pipeline backpressure', () => { + let network: TestNetworkNoAppView + let ingester: BskyIngester + let indexers: BskyIndexers + + let sc: SeedClient + + beforeAll(async () => { + network = await TestNetworkNoAppView.create({ + dbPostgresSchema: TEST_NAME, + }) + ingester = await getIngester(network, { + name: TEST_NAME, + ingesterPartitionCount: 2, + ingesterMaxItems: 10, + ingesterCheckItemsEveryN: 5, + }) + indexers = await getIndexers(network, { + name: TEST_NAME, + partitionIdsByIndexer: [[0], [1]], + }) + const pdsAgent = network.pds.getClient() + sc = new SeedClient(pdsAgent) + await basicSeed(sc) + }) + + afterAll(async () => { + await network.close() + }) + + it('ingester issues backpressure based on total of partition lengths.', async () => { + // ingest until first 10 are seen + await ingester.start() + while ((ingester.sub.lastSeq ?? 0) < 10) { + await wait(50) + } + // allow additional time to pass to ensure no additional events are being consumed + await wait(200) + // check that max items has been respected (i.e. backpressure was applied) + const lengths = await ingester.ctx.redis.streamLengths(['repo:0', 'repo:1']) + expect(lengths).toHaveLength(2) + expect(lengths[0] + lengths[1]).toBeLessThanOrEqual(10 + 5) // not exact due to batching, may catch on following check backpressure + // drain all items using indexers, releasing backpressure + await indexers.start() + await processAll(network, ingester) + const lengthsFinal = await ingester.ctx.redis.streamLengths([ + 'repo:0', + 'repo:1', + ]) + expect(lengthsFinal).toHaveLength(2) + expect(lengthsFinal[0] + lengthsFinal[1]).toEqual(0) + await indexers.destroy() + await ingester.destroy() + }) +}) diff --git a/packages/bsky/tests/pipeline/reingest.test.ts b/packages/bsky/tests/pipeline/reingest.test.ts new file mode 100644 index 00000000000..ed8afdfe36d --- /dev/null +++ b/packages/bsky/tests/pipeline/reingest.test.ts @@ -0,0 +1,52 @@ +import { TestNetworkNoAppView, getIngester, ingestAll } from '@atproto/dev-env' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' +import { BskyIngester } from '../../src' + +const TEST_NAME = 'pipeline_reingest' + +describe('pipeline reingestion', () => { + let network: TestNetworkNoAppView + let ingester: BskyIngester + let sc: SeedClient + + beforeAll(async () => { + network = await TestNetworkNoAppView.create({ + dbPostgresSchema: TEST_NAME, + }) + ingester = await getIngester(network, { + name: TEST_NAME, + ingesterPartitionCount: 1, + }) + const pdsAgent = network.pds.getClient() + sc = new SeedClient(pdsAgent) + await basicSeed(sc) + }) + + afterAll(async () => { + await network.close() + await ingester.destroy() + }) + + it('allows events to be reingested multiple times.', async () => { + // ingest all events once + await ingester.start() + await ingestAll(network, ingester) + const initialCursor = await ingester.sub.getCursor() + const [initialLen] = await ingester.ctx.redis.streamLengths(['repo:0']) + expect(initialCursor).toBeGreaterThan(10) + expect(initialLen).toBeGreaterThan(10) + // stop ingesting and reset ingester state + await ingester.sub.destroy() + await ingester.sub.resetCursor() + // add one new event and reingest + await sc.post(sc.dids.alice, 'one more event!') // add one event to firehose + ingester.sub.resume() + await ingestAll(network, ingester) + // confirm the newest event was ingested + const finalCursor = await ingester.sub.getCursor() + const [finalLen] = await ingester.ctx.redis.streamLengths(['repo:0']) + expect(finalCursor).toEqual(initialCursor + 1) + expect(finalLen).toEqual(initialLen + 1) + }) +}) diff --git a/packages/bsky/tests/pipeline/repartition.test.ts b/packages/bsky/tests/pipeline/repartition.test.ts new file mode 100644 index 00000000000..12205e56315 --- /dev/null +++ b/packages/bsky/tests/pipeline/repartition.test.ts @@ -0,0 +1,87 @@ +import { + BskyIndexers, + TestNetworkNoAppView, + getIndexers, + getIngester, + ingestAll, + processAll, +} from '@atproto/dev-env' +import { SeedClient } from '../seeds/client' +import usersSeed from '../seeds/users' +import { BskyIngester } from '../../src' +import { countAll } from '../../src/db/util' + +const TEST_NAME = 'pipeline_repartition' + +describe('pipeline indexer repartitioning', () => { + let network: TestNetworkNoAppView + let ingester: BskyIngester + let indexers1: BskyIndexers + let indexers2: BskyIndexers + let sc: SeedClient + + beforeAll(async () => { + network = await TestNetworkNoAppView.create({ + dbPostgresSchema: TEST_NAME, + }) + ingester = await getIngester(network, { + name: TEST_NAME, + ingesterPartitionCount: 2, + }) + indexers1 = await getIndexers(network, { + name: TEST_NAME, + partitionIdsByIndexer: [[0, 1]], // one indexer consuming two partitions + }) + indexers2 = await getIndexers(network, { + name: TEST_NAME, + partitionIdsByIndexer: [[0], [1]], // two indexers, each consuming one partition + }) + const pdsAgent = network.pds.getClient() + sc = new SeedClient(pdsAgent) + await usersSeed(sc) + }) + + afterAll(async () => { + await network.close() + }) + + it('indexers repartition without missing events.', async () => { + const poster = createPoster(sc) + await Promise.all([poster.post(4), indexers1.start(), ingester.start()]) + await poster.post(1) + await processAll(network, ingester) + const { count: indexedPosts } = await indexers1.db.db + .selectFrom('post') + .select(countAll.as('count')) + .executeTakeFirstOrThrow() + expect(indexedPosts).toEqual(5) + await Promise.all([poster.post(3), indexers1.destroy()]) + await poster.post(3) // miss some events + await ingestAll(network, ingester) + await Promise.all([poster.post(3), indexers2.start()]) // handle some events on indexers2 + await processAll(network, ingester) + const { count: allIndexedPosts } = await indexers2.db.db + .selectFrom('post') + .select(countAll.as('count')) + .executeTakeFirstOrThrow() + expect(allIndexedPosts).toBeGreaterThan(indexedPosts) + expect(allIndexedPosts).toEqual(poster.postCount) + await indexers2.destroy() + await ingester.destroy() + }) +}) + +function createPoster(sc: SeedClient) { + return { + postCount: 0, + destroyed: false, + async post(n = 1) { + const dids = Object.values(sc.dids) + for (let i = 0; i < n; ++i) { + const did = dids[this.postCount % dids.length] + await sc.post(did, `post ${this.postCount}`) + this.postCount++ + } + }, + } +} diff --git a/packages/bsky/tests/server.test.ts b/packages/bsky/tests/server.test.ts index e70660b7641..4974a2ea74d 100644 --- a/packages/bsky/tests/server.test.ts +++ b/packages/bsky/tests/server.test.ts @@ -111,7 +111,8 @@ describe('server', () => { }) it('healthcheck fails when database is unavailable.', async () => { - await network.bsky.sub.destroy() + await network.bsky.ingester.sub.destroy() + await network.bsky.indexer.sub.destroy() await db.close() let error: AxiosError try { diff --git a/packages/bsky/tests/subscription/repo.test.ts b/packages/bsky/tests/subscription/repo.test.ts index 3dfdd9ca796..23e32d9c130 100644 --- a/packages/bsky/tests/subscription/repo.test.ts +++ b/packages/bsky/tests/subscription/repo.test.ts @@ -66,9 +66,16 @@ describe('sync', () => { const originalTableDump = await getTableDump() // Reprocess repos via sync subscription, on top of existing indices - await network.bsky.sub?.destroy() - await network.bsky.sub?.resetState() - network.bsky.sub?.resume() + await network.bsky.ingester.sub.destroy() + await network.bsky.indexer.sub.destroy() + // Hard reset of state in redis + await network.bsky.ingester.sub.resetCursor() + const indexerSub = network.bsky.indexer.sub + const partition = indexerSub.partitions.get(0) + await network.bsky.indexer.ctx.redis.del(partition.key) + // Boot streams back up + network.bsky.indexer.sub.resume() + network.bsky.ingester.sub.resume() await network.processAll() // Permissive of indexedAt times changing @@ -98,6 +105,7 @@ describe('sync', () => { email: 'jack@test.com', password: 'password', }) + await network.pds.ctx.sequencerLeader?.isCaughtUp() await network.processAll() // confirm jack was indexed as an actor despite the bad event const actors = await dumpTable(ctx.db, 'actor', ['did']) diff --git a/packages/bsky/tests/views/actor-search.test.ts b/packages/bsky/tests/views/actor-search.test.ts index fa690f8c5cf..59c21587a19 100644 --- a/packages/bsky/tests/views/actor-search.test.ts +++ b/packages/bsky/tests/views/actor-search.test.ts @@ -21,7 +21,7 @@ describe('pds actor search views', () => { sc = new SeedClient(pdsAgent) await wait(50) // allow pending sub to be established - await network.bsky.sub?.destroy() + await network.bsky.ingester.sub.destroy() await usersBulkSeed(sc) // Skip did/handle resolution for expediency @@ -40,9 +40,9 @@ describe('pds actor search views', () => { .execute() // Process remaining profiles - network.bsky.sub?.resume() + network.bsky.ingester.sub.resume() await network.processAll(50000) - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() headers = await network.serviceHeaders(Object.values(sc.dids)[0]) }) diff --git a/packages/bsky/tests/views/admin/repo-search.test.ts b/packages/bsky/tests/views/admin/repo-search.test.ts index 8bcf5a00e7e..1474a562e53 100644 --- a/packages/bsky/tests/views/admin/repo-search.test.ts +++ b/packages/bsky/tests/views/admin/repo-search.test.ts @@ -18,8 +18,8 @@ describe('pds admin repo search views', () => { const pdsAgent = network.pds.getClient() sc = new SeedClient(pdsAgent) - await wait(50) // allow pending sub to be established - await network.bsky.sub?.destroy() + await wait(100) // allow pending sub to be established + await network.bsky.ingester.sub.destroy() await usersBulkSeed(sc) // Skip did/handle resolution for expediency @@ -38,7 +38,7 @@ describe('pds admin repo search views', () => { .execute() // Process remaining profiles - network.bsky.sub?.resume() + network.bsky.ingester.sub.resume() await network.processAll(50000) headers = await network.adminHeaders({}) }) diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index 88fd1ec7579..11c79189c4d 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -25,7 +25,6 @@ describe('pds author feed views', () => { sc = new SeedClient(pdsAgent) await basicSeed(sc) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() alice = sc.dids.alice bob = sc.dids.bob carol = sc.dids.carol diff --git a/packages/bsky/tests/views/follows.test.ts b/packages/bsky/tests/views/follows.test.ts index 8e8903c45d5..e048b433b8e 100644 --- a/packages/bsky/tests/views/follows.test.ts +++ b/packages/bsky/tests/views/follows.test.ts @@ -22,7 +22,7 @@ describe('pds follow views', () => { sc = new SeedClient(pdsAgent) await followsSeed(sc) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() alice = sc.dids.alice }) diff --git a/packages/bsky/tests/views/mute-lists.test.ts b/packages/bsky/tests/views/mute-lists.test.ts index f130ac508e4..fd2d02ed81f 100644 --- a/packages/bsky/tests/views/mute-lists.test.ts +++ b/packages/bsky/tests/views/mute-lists.test.ts @@ -81,7 +81,6 @@ describe('bsky views with mutes from mute lists', () => { sc.getHeaders(alice), ) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() }) it('uses a list for mutes', async () => { @@ -166,7 +165,7 @@ describe('bsky views with mutes from mute lists', () => { // unfollow so they _would_ show up in suggestions if not for mute await sc.unfollow(dan, carol) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() const res = await agent.api.app.bsky.actor.getSuggestions( { diff --git a/packages/bsky/tests/views/notifications.test.ts b/packages/bsky/tests/views/notifications.test.ts index 1db9dbf67a1..7bdf6c85666 100644 --- a/packages/bsky/tests/views/notifications.test.ts +++ b/packages/bsky/tests/views/notifications.test.ts @@ -23,7 +23,7 @@ describe('notification views', () => { sc = new SeedClient(pdsAgent) await basicSeed(sc) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() alice = sc.dids.alice }) @@ -76,7 +76,7 @@ describe('notification views', () => { 'indeed', ) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() const notifCountAlice = await agent.api.app.bsky.notification.getUnreadCount( @@ -100,7 +100,7 @@ describe('notification views', () => { await sc.deletePost(sc.dids.alice, root.ref.uri) const second = await sc.reply(sc.dids.carol, root.ref, first.ref, 'second') await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() const notifsAlice = await agent.api.app.bsky.notification.listNotifications( {}, diff --git a/packages/bsky/tests/views/posts.test.ts b/packages/bsky/tests/views/posts.test.ts index 676d03ccd59..99ec565dc59 100644 --- a/packages/bsky/tests/views/posts.test.ts +++ b/packages/bsky/tests/views/posts.test.ts @@ -18,7 +18,7 @@ describe('pds posts views', () => { sc = new SeedClient(pdsAgent) await basicSeed(sc) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() }) afterAll(async () => { diff --git a/packages/bsky/tests/views/profile.test.ts b/packages/bsky/tests/views/profile.test.ts index 36a5937c6af..ecdbf983e6b 100644 --- a/packages/bsky/tests/views/profile.test.ts +++ b/packages/bsky/tests/views/profile.test.ts @@ -27,7 +27,7 @@ describe('pds profile views', () => { sc = new SeedClient(pdsAgent) await basicSeed(sc) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() alice = sc.dids.alice bob = sc.dids.bob dan = sc.dids.dan diff --git a/packages/bsky/tests/views/suggestions.test.ts b/packages/bsky/tests/views/suggestions.test.ts index 85bf3fc3a4f..9272a9514cd 100644 --- a/packages/bsky/tests/views/suggestions.test.ts +++ b/packages/bsky/tests/views/suggestions.test.ts @@ -18,7 +18,7 @@ describe('pds user search views', () => { sc = new SeedClient(pdsAgent) await basicSeed(sc) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() const suggestions = [ { did: sc.dids.bob, order: 1 }, diff --git a/packages/bsky/tests/views/thread.test.ts b/packages/bsky/tests/views/thread.test.ts index c3f5cb274dc..7df31e24b87 100644 --- a/packages/bsky/tests/views/thread.test.ts +++ b/packages/bsky/tests/views/thread.test.ts @@ -32,7 +32,7 @@ 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.ctx.backgroundQueue.processAll() + await network.bsky.processAll() }) afterAll(async () => { @@ -116,7 +116,7 @@ describe('pds thread views', () => { ) indexes.aliceReplyReply = sc.replies[alice].length - 1 await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() const thread1 = await agent.api.app.bsky.feed.getPostThread( { uri: sc.posts[alice][indexes.aliceRoot].ref.uriStr }, @@ -126,7 +126,7 @@ describe('pds thread views', () => { await sc.deletePost(bob, sc.replies[bob][indexes.bobReply].ref.uri) await network.processAll() - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() const thread2 = await agent.api.app.bsky.feed.getPostThread( { uri: sc.posts[alice][indexes.aliceRoot].ref.uriStr }, diff --git a/packages/bsky/tests/views/timeline.test.ts b/packages/bsky/tests/views/timeline.test.ts index 436684e0186..c1cf35ce8fa 100644 --- a/packages/bsky/tests/views/timeline.test.ts +++ b/packages/bsky/tests/views/timeline.test.ts @@ -50,7 +50,7 @@ describe('timeline views', () => { labelPostB.cidStr, { create: ['kind'] }, ) - await network.bsky.ctx.backgroundQueue.processAll() + await network.bsky.processAll() }) afterAll(async () => { diff --git a/packages/common/src/buffers.ts b/packages/common/src/buffers.ts new file mode 100644 index 00000000000..8f03ea29845 --- /dev/null +++ b/packages/common/src/buffers.ts @@ -0,0 +1,10 @@ +export function ui8ToBuffer(bytes: Uint8Array): Buffer { + return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength) +} + +export function ui8ToArrayBuffer(bytes: Uint8Array): ArrayBuffer { + return bytes.buffer.slice( + bytes.byteOffset, + bytes.byteLength + bytes.byteOffset, + ) +} diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 8fd068c8602..fd049fc2e5d 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -4,3 +4,4 @@ export * from './ipld' export * from './ipld-multi' export * from './logger' export * from './streams' +export * from './buffers' diff --git a/packages/crypto/tests/random.test.ts b/packages/crypto/tests/random.test.ts new file mode 100644 index 00000000000..349bf0c2f27 --- /dev/null +++ b/packages/crypto/tests/random.test.ts @@ -0,0 +1,15 @@ +import { randomIntFromSeed } from '../src' + +describe('randomIntFromSeed()', () => { + it('has good distribution for low bucket count.', async () => { + const counts: [zero: number, one: number] = [0, 0] + const salt = Math.random() + for (let i = 0; i < 10000; ++i) { + const int = await randomIntFromSeed(`${i}${salt}`, 2) + counts[int]++ + } + const [zero, one] = counts + expect(zero + one).toEqual(10000) + expect(Math.max(zero, one) / Math.min(zero, one)).toBeLessThan(1.05) + }) +}) diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index 03ad48fc796..46a2dd0f793 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -1,18 +1,22 @@ +import assert from 'assert' import getPort from 'get-port' import * as ui8 from 'uint8arrays' import * as bsky from '@atproto/bsky' -import { DAY, HOUR } from '@atproto/common-web' +import { DAY, HOUR, wait } from '@atproto/common-web' import { AtpAgent } from '@atproto/api' -import { Secp256k1Keypair } from '@atproto/crypto' +import { Secp256k1Keypair, randomIntFromSeed } from '@atproto/crypto' import { Client as PlcClient } from '@did-plc/lib' import { BskyConfig } from './types' import { uniqueLockId } from './util' +import { TestNetworkNoAppView } from './network-no-appview' export class TestBsky { constructor( public url: string, public port: number, public server: bsky.BskyAppView, + public indexer: bsky.BskyIndexer, + public ingester: bsky.BskyIngester, ) {} static async create(cfg: BskyConfig): Promise { @@ -42,18 +46,18 @@ export class TestBsky { didCacheMaxTTL: DAY, ...cfg, // Each test suite gets its own lock id for the repo subscription - repoSubLockId: uniqueLockId(), adminPassword: 'admin-pass', moderatorPassword: 'moderator-pass', triagePassword: 'triage-pass', labelerDid: 'did:example:labeler', - labelerKeywords: { label_me: 'test-label', label_me_2: 'test-label-2' }, feedGenDid: 'did:example:feedGen', }) + // shared across server, ingester, and indexer in order to share pool, avoid too many pg connections. const db = bsky.Database.postgres({ url: cfg.dbPostgresUrl, schema: cfg.dbPostgresSchema, + poolSize: 10, }) // Separate migration db in case migration changes some connection state that we need in the tests, e.g. "alter database ... set ..." @@ -68,10 +72,61 @@ export class TestBsky { } await migrationDb.close() + // api server const server = bsky.BskyAppView.create({ db, config, algos: cfg.algos }) + // indexer + const ns = cfg.dbPostgresSchema + ? await randomIntFromSeed(cfg.dbPostgresSchema, 10000) + : undefined + const indexerCfg = new bsky.IndexerConfig({ + version: '0.0.0', + didCacheStaleTTL: HOUR, + didCacheMaxTTL: DAY, + labelerDid: 'did:example:labeler', + redisHost: cfg.redisHost, + dbPostgresUrl: cfg.dbPostgresUrl, + dbPostgresSchema: cfg.dbPostgresSchema, + didPlcUrl: cfg.plcUrl, + labelerKeywords: { label_me: 'test-label', label_me_2: 'test-label-2' }, + indexerPartitionIds: [0], + indexerNamespace: `ns${ns}`, + indexerSubLockId: uniqueLockId(), + }) + assert(indexerCfg.redisHost) + const indexerRedis = new bsky.Redis({ + host: indexerCfg.redisHost, + namespace: indexerCfg.indexerNamespace, + }) + const indexer = bsky.BskyIndexer.create({ + cfg: indexerCfg, + db, + redis: indexerRedis, + }) + // ingester + const ingesterCfg = new bsky.IngesterConfig({ + version: '0.0.0', + redisHost: cfg.redisHost, + dbPostgresUrl: cfg.dbPostgresUrl, + dbPostgresSchema: cfg.dbPostgresSchema, + repoProvider: cfg.repoProvider, + ingesterNamespace: `ns${ns}`, + ingesterSubLockId: uniqueLockId(), + ingesterPartitionCount: 1, + }) + assert(ingesterCfg.redisHost) + const ingesterRedis = new bsky.Redis({ + host: ingesterCfg.redisHost, + namespace: ingesterCfg.ingesterNamespace, + }) + const ingester = bsky.BskyIngester.create({ + cfg: ingesterCfg, + db, + redis: ingesterRedis, + }) + await ingester.start() + await indexer.start() await server.start() - - return new TestBsky(url, port, server) + return new TestBsky(url, port, server, indexer, ingester) } get ctx(): bsky.AppContext { @@ -79,10 +134,7 @@ export class TestBsky { } get sub() { - if (!this.server.sub) { - throw new Error('No subscription on dev-env server') - } - return this.server.sub + return this.indexer.sub } getClient() { @@ -109,10 +161,162 @@ export class TestBsky { } async processAll() { - await this.ctx.backgroundQueue.processAll() + await Promise.all([ + this.ctx.backgroundQueue.processAll(), + this.indexer.ctx.backgroundQueue.processAll(), + ]) } async close() { - await this.server.destroy() + await this.server.destroy({ skipDb: true }) + await this.ingester.destroy({ skipDb: true }) + this.indexer.destroy() // closes shared db + } +} + +// Below are used for tests just of component parts of the appview, i.e. ingester and indexers: + +export async function getIngester( + network: TestNetworkNoAppView, + opts: { name: string } & Partial, +) { + const { name, ...config } = opts + const ns = name ? await randomIntFromSeed(name, 10000) : undefined + const cfg = new bsky.IngesterConfig({ + version: '0.0.0', + redisHost: process.env.REDIS_HOST || '', + dbPostgresUrl: process.env.DB_POSTGRES_URL || '', + dbPostgresSchema: `appview_${name}`, + repoProvider: network.pds.url.replace('http://', 'ws://'), + ingesterSubLockId: uniqueLockId(), + ingesterPartitionCount: config.ingesterPartitionCount ?? 1, + ingesterNamespace: `ns${ns}`, + ...config, + }) + const db = bsky.Database.postgres({ + url: cfg.dbPostgresUrl, + schema: cfg.dbPostgresSchema, + }) + assert(cfg.redisHost) + const redis = new bsky.Redis({ + host: cfg.redisHost, + namespace: cfg.ingesterNamespace, + }) + await db.migrateToLatestOrThrow() + return bsky.BskyIngester.create({ cfg, db, redis }) +} + +// get multiple indexers for separate partitions, sharing db and redis instance. +export async function getIndexers( + network: TestNetworkNoAppView, + opts: Partial & { + name: string + partitionIdsByIndexer: number[][] + }, +): Promise { + const { name, ...config } = opts + const ns = name ? await randomIntFromSeed(name, 10000) : undefined + const baseCfg: bsky.IndexerConfigValues = { + version: '0.0.0', + 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 || '', + dbPostgresSchema: `appview_${name}`, + didPlcUrl: network.plc.url, + indexerPartitionIds: [0], + indexerNamespace: `ns${ns}`, + ...config, + } + const db = bsky.Database.postgres({ + url: baseCfg.dbPostgresUrl, + schema: baseCfg.dbPostgresSchema, + }) + assert(baseCfg.redisHost) + const redis = new bsky.Redis({ + host: baseCfg.redisHost, + namespace: baseCfg.indexerNamespace, + }) + const indexers = opts.partitionIdsByIndexer.map((indexerPartitionIds) => { + const cfg = new bsky.IndexerConfig({ + ...baseCfg, + indexerPartitionIds, + indexerSubLockId: uniqueLockId(), + }) + return bsky.BskyIndexer.create({ cfg, db, redis }) + }) + await db.migrateToLatestOrThrow() + return { + db, + list: indexers, + async start() { + await Promise.all(indexers.map((indexer) => indexer.start())) + }, + async destroy() { + const stopping = [...indexers] + const lastIndexer = stopping.pop() + await Promise.all( + stopping.map((indexer) => + indexer.destroy({ skipDb: true, skipRedis: true }), + ), + ) + await lastIndexer?.destroy() + }, + } +} + +export type BskyIndexers = { + db: bsky.Database + list: bsky.BskyIndexer[] + start(): Promise + destroy(): Promise +} + +export async function processAll( + network: TestNetworkNoAppView, + ingester: bsky.BskyIngester, +) { + assert(network.pds.ctx.sequencerLeader, 'sequencer leader does not exist') + await network.pds.processAll() + await ingestAll(network, ingester) + // eslint-disable-next-line no-constant-condition + while (true) { + // check indexers + const keys = [...Array(ingester.sub.opts.partitionCount)].map( + (_, i) => `repo:${i}`, + ) + const results = await ingester.sub.ctx.redis.streamLengths(keys) + const indexersCaughtUp = results.every((len) => len === 0) + if (indexersCaughtUp) return + await wait(50) + } +} + +export async function ingestAll( + network: TestNetworkNoAppView, + ingester: bsky.BskyIngester, +) { + assert(network.pds.ctx.sequencerLeader, 'sequencer leader does not exist') + const pdsDb = network.pds.ctx.db.db + await network.pds.processAll() + // eslint-disable-next-line no-constant-condition + while (true) { + await wait(50) + // check sequencer + const sequencerCaughtUp = await network.pds.ctx.sequencerLeader.isCaughtUp() + if (!sequencerCaughtUp) continue + // check ingester + const [ingesterCursor, { lastSeq }] = await Promise.all([ + ingester.sub.getCursor(), + pdsDb + .selectFrom('repo_seq') + .where('seq', 'is not', null) + .select(pdsDb.fn.max('repo_seq.seq').as('lastSeq')) + .executeTakeFirstOrThrow(), + ]) + const ingesterCaughtUp = ingesterCursor === lastSeq + if (ingesterCaughtUp) return } } diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index 353216b5279..4daf69a541d 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -21,8 +21,10 @@ export class TestNetwork extends TestNetworkNoAppView { static async create( params: Partial = {}, ): Promise { + const redisHost = process.env.REDIS_HOST const dbPostgresUrl = params.dbPostgresUrl || process.env.DB_POSTGRES_URL assert(dbPostgresUrl, 'Missing postgres url for tests') + assert(redisHost, 'Missing redis host for tests') const dbPostgresSchema = params.dbPostgresSchema || process.env.DB_POSTGRES_SCHEMA @@ -36,6 +38,7 @@ export class TestNetwork extends TestNetworkNoAppView { repoProvider: `ws://localhost:${pdsPort}`, dbPostgresSchema: `appview_${dbPostgresSchema}`, dbPostgresUrl, + redisHost, ...params.bsky, }) const pds = await TestPds.create({ @@ -54,14 +57,11 @@ export class TestNetwork extends TestNetworkNoAppView { } async processFullSubscription(timeout = 5000) { - const sub = this.bsky.sub - if (!sub) return + const sub = this.bsky.indexer.sub const { db } = this.pds.ctx.db const start = Date.now() while (Date.now() - start < timeout) { await wait(50) - if (!sub) return - const state = await sub.getState() if (!this.pds.ctx.sequencerLeader) { throw new Error('Sequencer leader not configured on the pds') } @@ -72,7 +72,12 @@ export class TestNetwork extends TestNetworkNoAppView { .where('seq', 'is not', null) .select(db.fn.max('repo_seq.seq').as('lastSeq')) .executeTakeFirstOrThrow() - if (state.cursor === lastSeq) return + const { cursor } = sub.partitions.get(0) + if (cursor === lastSeq) { + // has seen last seq, just need to wait for it to finish processing + await sub.repoQueue.main.onIdle() + return + } } throw new Error(`Sequence was not processed within ${timeout}ms`) } diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index 927a3364ccc..654adc4c474 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -17,6 +17,7 @@ export type BskyConfig = Partial & { plcUrl: string repoProvider: string dbPostgresUrl: string + redisHost: string migration?: string algos?: bsky.MountedAlgos } diff --git a/packages/dev-env/src/util.ts b/packages/dev-env/src/util.ts index 1535f0bbf55..340d0414da1 100644 --- a/packages/dev-env/src/util.ts +++ b/packages/dev-env/src/util.ts @@ -1,11 +1,12 @@ -import { DidResolver, HandleResolver, IdResolver } from '@atproto/identity' +import { IdResolver } from '@atproto/identity' import { TestPds } from './pds' import { TestBsky } from './bsky' -export const mockNetworkUtilities = async (pds: TestPds, bsky?: TestBsky) => { - await mockResolvers(pds.ctx.idResolver, pds) +export const mockNetworkUtilities = (pds: TestPds, bsky?: TestBsky) => { + mockResolvers(pds.ctx.idResolver, pds) if (bsky) { - await mockResolvers(bsky.ctx.idResolver, pds) + mockResolvers(bsky.ctx.idResolver, pds) + mockResolvers(bsky.indexer.ctx.idResolver, pds) } } diff --git a/packages/pg/README.md b/packages/dev-infra/README.md similarity index 67% rename from packages/pg/README.md rename to packages/dev-infra/README.md index 774500f3b7b..e4864d7a8ce 100644 --- a/packages/pg/README.md +++ b/packages/dev-infra/README.md @@ -1,6 +1,6 @@ -# pg +# dev-infra -Helpers for working with postgres +Helpers for working with postgres and redis locally. Previously known as `pg`. ## Usage @@ -30,6 +30,12 @@ Going to remove pg-db_test-1 ⠿ Container pg-db_test-1 Removed ``` +### `with-redis-and-test-db.sh` + +This script is similar to `with-test-db.sh`, but in addition to an ephemeral/single-use postgres database it also provies a single-use redis instance. When the script starts, Dockerized postgres and redis containers start-up, and when the script completes the containers are removed. + +The environment variables `DB_POSTGRES_URL` and `REDIS_HOST` will be set with a connection strings that can be used to connect to postgres and redis respectively. + ### `docker-compose.yaml` The Docker compose file can be used to run containerized versions of postgres either for single use (as is used by `with-test-db.sh`), or for longer-term use. These are setup as separate services named `db_test` and `db` respectively. In both cases the database is available on the host machine's `localhost` and credentials are: @@ -63,3 +69,15 @@ $ docker compose stop db # stop container $ docker compose rm db # remove container $ docker volume rm pg_atp_db # remove volume ``` + +#### `redis_test` service for single use + +The single-use `redis_test` service does not have any persistent storage. When the container is removed, the data in redis disappears with it. + +This service runs on port `6380`. + +#### `redis` service for persistent use + +The `redis` service has persistent storage on the host machine managed by Docker under a volume named `atp_redis`. When the container is removed, the data in redis will remain on the host machine. In order to start fresh, you would need to remove the volume. + +This service runs on port `6379`. diff --git a/packages/dev-infra/_common.sh b/packages/dev-infra/_common.sh new file mode 100755 index 00000000000..0d66653c878 --- /dev/null +++ b/packages/dev-infra/_common.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env sh + +get_container_id() { + local compose_file=$1 + local service=$2 + if [ -z "${compose_file}" ] || [ -z "${service}" ]; then + echo "usage: get_container_id " + exit 1 + fi + + docker compose -f $compose_file ps --format json --status running \ + | jq -r '.[]? | select(.Service == "'${service}'") | .ID' +} + +# Exports all environment variables +export_env() { + export_pg_env + export_redis_env +} + +# Exports postgres environment variables +export_pg_env() { + # Based on creds in compose.yaml + export PGPORT=5433 + export PGHOST=localhost + export PGUSER=pg + export PGPASSWORD=password + export PGDATABASE=postgres + export DB_POSTGRES_URL="postgresql://pg:password@127.0.0.1:5433/postgres" +} + +# Exports redis environment variables +export_redis_env() { + export REDIS_HOST="127.0.0.1:6380" +} + +# Main entry point +main() { + # Expect a SERVICES env var to be set with the docker service names + local services=${SERVICES} + + dir=$(dirname $0) + compose_file="${dir}/docker-compose.yaml" + + # whether this particular script started the container(s) + started_container=false + + # trap SIGINT and performs cleanup as necessary, i.e. + # taking down containers if this script started them + trap "on_sigint ${services}" INT + on_sigint() { + local services=$@ + echo # newline + if $started_container; then + docker compose -f $compose_file rm -f --stop --volumes ${services} + fi + exit $? + } + + # check if all services are running already + not_running=false + for service in $services; do + container_id=$(get_container_id $compose_file $service) + if [ -z $container_id ]; then + not_running=true + break + fi + done + + # if any are missing, recreate all services + if $not_running; then + docker compose -f $compose_file up --wait --force-recreate ${services} + started_container=true + else + echo "all services ${services} are already running" + fi + + # setup environment variables and run args + export_env + "$@" + # save return code for later + code=$? + + # performs cleanup as necessary, i.e. taking down containers + # if this script started them + echo # newline + if $started_container; then + docker compose -f $compose_file rm -f --stop --volumes ${services} + fi + + exit ${code} +} diff --git a/packages/pg/docker-compose.yaml b/packages/dev-infra/docker-compose.yaml similarity index 51% rename from packages/pg/docker-compose.yaml rename to packages/dev-infra/docker-compose.yaml index 033afa3bb7c..3d582c18b37 100644 --- a/packages/pg/docker-compose.yaml +++ b/packages/dev-infra/docker-compose.yaml @@ -23,5 +23,27 @@ services: disable: true volumes: - atp_db:/var/lib/postgresql/data + # An ephermerally-stored redis cache for single-use test runs + redis_test: &redis_test + image: redis:7.0-alpine + ports: + - '6380:6379' + # Healthcheck ensures redis is queryable when `docker-compose up --wait` completes + healthcheck: + test: ['CMD-SHELL', '[ "$$(redis-cli ping)" = "PONG" ]'] + interval: 500ms + timeout: 10s + retries: 20 + # A persistently-stored redis cache + redis: + <<: *redis_test + command: redis-server --save 60 1 --loglevel warning + ports: + - '6379:6379' + healthcheck: + disable: true + volumes: + - atp_redis:/data volumes: atp_db: + atp_redis: diff --git a/packages/dev-infra/with-test-db.sh b/packages/dev-infra/with-test-db.sh new file mode 100755 index 00000000000..cc083491a59 --- /dev/null +++ b/packages/dev-infra/with-test-db.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env sh + +# Example usage: +# ./with-test-db.sh psql postgresql://pg:password@localhost:5433/postgres -c 'select 1;' + +dir=$(dirname $0) +. ${dir}/_common.sh + +SERVICES="db_test" main "$@" diff --git a/packages/dev-infra/with-test-redis-and-db.sh b/packages/dev-infra/with-test-redis-and-db.sh new file mode 100755 index 00000000000..c2b0c75ff14 --- /dev/null +++ b/packages/dev-infra/with-test-redis-and-db.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +# Example usage: +# ./with-test-redis-and-db.sh psql postgresql://pg:password@localhost:5433/postgres -c 'select 1;' +# ./with-test-redis-and-db.sh redis-cli -h localhost -p 6380 ping + +dir=$(dirname $0) +. ${dir}/_common.sh + +SERVICES="db_test redis_test" main "$@" diff --git a/packages/pds/package.json b/packages/pds/package.json index 89781642f74..4690a0ba665 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -14,8 +14,8 @@ "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "start": "node dist/bin.js", - "test": "../pg/with-test-db.sh jest", - "bench": "../pg/with-test-db.sh jest --config jest.bench.config.js", + "test": "../dev-infra/with-test-redis-and-db.sh jest", + "bench": "../dev-infra/with-test-redis-and-db.sh jest --config jest.bench.config.js", "test:sqlite": "jest --testPathIgnorePatterns /tests/proxied/*", "test:log": "tail -50 test.log | pino-pretty", "test:updateSnapshot": "jest --updateSnapshot", diff --git a/packages/pds/tests/crud.test.ts b/packages/pds/tests/crud.test.ts index d7c4d094a36..00f7c778009 100644 --- a/packages/pds/tests/crud.test.ts +++ b/packages/pds/tests/crud.test.ts @@ -5,7 +5,7 @@ import * as createRecord from '@atproto/api/src/client/types/com/atproto/repo/cr import * as putRecord from '@atproto/api/src/client/types/com/atproto/repo/putRecord' import * as deleteRecord from '@atproto/api/src/client/types/com/atproto/repo/deleteRecord' import * as applyWrites from '@atproto/api/src/client/types/com/atproto/repo/applyWrites' -import { cidForCbor, TID } from '@atproto/common' +import { cidForCbor, TID, ui8ToArrayBuffer } from '@atproto/common' import { BlobNotFoundError } from '@atproto/repo' import { defaultFetchHandler } from '@atproto/xrpc' import * as Post from '../src/lexicon/types/app/bsky/feed/post' @@ -833,7 +833,7 @@ describe('crud operations', () => { it("writes fail on values that can't reliably transform between cbor to lex", async () => { const passthroughBody = (data: unknown) => - typedArrayToBuffer(new TextEncoder().encode(JSON.stringify(data))) + ui8ToArrayBuffer(new TextEncoder().encode(JSON.stringify(data))) const result = await defaultFetchHandler( aliceAgent.service.origin + `/xrpc/com.atproto.repo.createRecord`, 'post', @@ -1211,10 +1211,3 @@ function createDeepObject(depth: number) { } return obj } - -function typedArrayToBuffer(array: Uint8Array): ArrayBuffer { - return array.buffer.slice( - array.byteOffset, - array.byteLength + array.byteOffset, - ) -} diff --git a/packages/pg/with-test-db.sh b/packages/pg/with-test-db.sh deleted file mode 100755 index 04f11f05852..00000000000 --- a/packages/pg/with-test-db.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env sh - -# Example usage: -# ./with-test-db.sh psql postgresql://pg:password@localhost:5433/postgres -c 'select 1;' - -dir=$(dirname $0) -compose_file="$dir/docker-compose.yaml" - -# whether this particular script started the container or if it was running beforehand -started_container=false - -trap on_sigint INT -on_sigint() { - echo # newline - if $started_container; then - docker compose -f $compose_file rm -f --stop --volumes db_test - fi - exit $? -} - -# check if the container is already running -container_id=$(docker compose -f $compose_file ps --format json --status running | jq --raw-output '.[0]."ID"') -if [ -z $container_id ] || [ -z `docker ps -q --no-trunc | grep $container_id` ]; then - docker compose -f $compose_file up --wait --force-recreate db_test - started_container=true - echo # newline -else - echo "db_test already running" -fi - -# Based on creds in compose.yaml -export PGPORT=5433 -export PGHOST=localhost -export PGUSER=pg -export PGPASSWORD=password -export PGDATABASE=postgres -export DB_POSTGRES_URL="postgresql://pg:password@127.0.0.1:5433/postgres" -"$@" -code=$? - -echo # newline -if $started_container; then - docker compose -f $compose_file rm -f --stop --volumes db_test -fi - -exit $code diff --git a/yarn.lock b/yarn.lock index 957fe4fd0cf..0bc37b7b7eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3352,6 +3352,11 @@ resolved "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz" integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== +"@ioredis/commands@^1.1.1": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" + integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== + "@ipld/car@^3.2.3": version "3.2.4" resolved "https://registry.yarnpkg.com/@ipld/car/-/car-3.2.4.tgz#115951ba2255ec51d865773a074e422c169fb01c" @@ -5828,6 +5833,11 @@ clone@^1.0.2: resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== +cluster-key-slot@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + cmd-shim@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/cmd-shim/-/cmd-shim-4.1.0.tgz" @@ -6269,6 +6279,11 @@ delegates@^1.0.0: resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + depd@2.0.0, depd@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" @@ -7904,6 +7919,21 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +ioredis@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.3.2.tgz#9139f596f62fc9c72d873353ac5395bcf05709f7" + integrity sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA== + dependencies: + "@ioredis/commands" "^1.1.1" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.1.0" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ip@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz" @@ -8825,11 +8855,21 @@ lodash.debounce@^4.0.8: resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz" integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== + lodash.isboolean@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz" @@ -10565,6 +10605,18 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== + dependencies: + redis-errors "^1.0.0" + regenerate-unicode-properties@^10.1.0: version "10.1.0" resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz" @@ -11131,6 +11183,11 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + statuses@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" From 13b16b33f2e5459c33800f75b7cd2deb86b51649 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Wed, 2 Aug 2023 00:43:21 +0200 Subject: [PATCH 079/237] Allow setting a note when enabling/disabling invite codes (#1363) * Allow setting a note when enabling/disabling invite codes * Add inviteNote to repoView * :white_check_mark: Add test for inviteNote * :sparkles: Set to null when note is empty * :sparkles: Change note from text to varchar * :memo: Better description --- lexicons/com/atproto/admin/defs.json | 6 ++-- .../atproto/admin/disableAccountInvites.json | 6 +++- .../atproto/admin/enableAccountInvites.json | 6 +++- packages/api/src/client/lexicons.ts | 16 ++++++++++ .../client/types/com/atproto/admin/defs.ts | 2 ++ .../atproto/admin/disableAccountInvites.ts | 2 ++ .../com/atproto/admin/enableAccountInvites.ts | 2 ++ packages/bsky/src/lexicon/lexicons.ts | 16 ++++++++++ .../lexicon/types/com/atproto/admin/defs.ts | 2 ++ .../atproto/admin/disableAccountInvites.ts | 2 ++ .../com/atproto/admin/enableAccountInvites.ts | 2 ++ .../atproto/admin/disableAccountInvites.ts | 4 +-- .../com/atproto/admin/enableAccountInvites.ts | 4 +-- .../20230801T141349990Z-invite-note.ts | 12 +++++++ packages/pds/src/db/migrations/index.ts | 1 + packages/pds/src/db/tables/user-account.ts | 1 + packages/pds/src/lexicon/lexicons.ts | 16 ++++++++++ .../lexicon/types/com/atproto/admin/defs.ts | 2 ++ .../atproto/admin/disableAccountInvites.ts | 2 ++ .../com/atproto/admin/enableAccountInvites.ts | 2 ++ packages/pds/src/services/moderation/views.ts | 5 ++- .../pds/tests/views/admin/invites.test.ts | 32 ++++++++++++++++++- 22 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 packages/pds/src/db/migrations/20230801T141349990Z-invite-note.ts diff --git a/lexicons/com/atproto/admin/defs.json b/lexicons/com/atproto/admin/defs.json index 03bbced34b6..62235b95213 100644 --- a/lexicons/com/atproto/admin/defs.json +++ b/lexicons/com/atproto/admin/defs.json @@ -192,7 +192,8 @@ "type": "ref", "ref": "com.atproto.server.defs#inviteCode" }, - "invitesDisabled": { "type": "boolean" } + "invitesDisabled": { "type": "boolean" }, + "inviteNote": { "type": "string" } } }, "repoViewDetail": { @@ -226,7 +227,8 @@ "ref": "com.atproto.server.defs#inviteCode" } }, - "invitesDisabled": { "type": "boolean" } + "invitesDisabled": { "type": "boolean" }, + "inviteNote": { "type": "string" } } }, "repoViewNotFound": { diff --git a/lexicons/com/atproto/admin/disableAccountInvites.json b/lexicons/com/atproto/admin/disableAccountInvites.json index 58d72c11564..2006271d089 100644 --- a/lexicons/com/atproto/admin/disableAccountInvites.json +++ b/lexicons/com/atproto/admin/disableAccountInvites.json @@ -11,7 +11,11 @@ "type": "object", "required": ["account"], "properties": { - "account": {"type": "string", "format": "did"} + "account": { "type": "string", "format": "did" }, + "note": { + "type": "string", + "description": "Additionally add a note describing why the invites were disabled" + } } } } diff --git a/lexicons/com/atproto/admin/enableAccountInvites.json b/lexicons/com/atproto/admin/enableAccountInvites.json index 9519a15a7c0..ac42d727879 100644 --- a/lexicons/com/atproto/admin/enableAccountInvites.json +++ b/lexicons/com/atproto/admin/enableAccountInvites.json @@ -11,7 +11,11 @@ "type": "object", "required": ["account"], "properties": { - "account": {"type": "string", "format": "did"} + "account": { "type": "string", "format": "did" }, + "note": { + "type": "string", + "description": "Additionally add a note describing why the invites were enabled" + } } } } diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 9fece7ebbfa..5a607b29625 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -343,6 +343,9 @@ export const schemaDict = { invitesDisabled: { type: 'boolean', }, + inviteNote: { + type: 'string', + }, }, }, repoViewDetail: { @@ -401,6 +404,9 @@ export const schemaDict = { invitesDisabled: { type: 'boolean', }, + inviteNote: { + type: 'string', + }, }, }, repoViewNotFound: { @@ -640,6 +646,11 @@ export const schemaDict = { type: 'string', format: 'did', }, + note: { + type: 'string', + description: + 'Additionally add a note describing why the invites were disabled', + }, }, }, }, @@ -694,6 +705,11 @@ export const schemaDict = { type: 'string', format: 'did', }, + note: { + type: 'string', + description: + 'Additionally add a note describing why the invites were disabled', + }, }, }, }, 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 7da21d1178e..ad2762566d6 100644 --- a/packages/api/src/client/types/com/atproto/admin/defs.ts +++ b/packages/api/src/client/types/com/atproto/admin/defs.ts @@ -189,6 +189,7 @@ export interface RepoView { moderation: Moderation invitedBy?: ComAtprotoServerDefs.InviteCode invitesDisabled?: boolean + inviteNote?: string [k: string]: unknown } @@ -215,6 +216,7 @@ export interface RepoViewDetail { invitedBy?: ComAtprotoServerDefs.InviteCode invites?: ComAtprotoServerDefs.InviteCode[] invitesDisabled?: boolean + inviteNote?: string [k: string]: unknown } diff --git a/packages/api/src/client/types/com/atproto/admin/disableAccountInvites.ts b/packages/api/src/client/types/com/atproto/admin/disableAccountInvites.ts index 3e224c0aa1a..cf61d3026d4 100644 --- a/packages/api/src/client/types/com/atproto/admin/disableAccountInvites.ts +++ b/packages/api/src/client/types/com/atproto/admin/disableAccountInvites.ts @@ -11,6 +11,8 @@ export interface QueryParams {} export interface InputSchema { account: string + /** Additionally add a note describing why the invites were disabled */ + note?: string [k: string]: unknown } diff --git a/packages/api/src/client/types/com/atproto/admin/enableAccountInvites.ts b/packages/api/src/client/types/com/atproto/admin/enableAccountInvites.ts index 3e224c0aa1a..cf61d3026d4 100644 --- a/packages/api/src/client/types/com/atproto/admin/enableAccountInvites.ts +++ b/packages/api/src/client/types/com/atproto/admin/enableAccountInvites.ts @@ -11,6 +11,8 @@ export interface QueryParams {} export interface InputSchema { account: string + /** Additionally add a note describing why the invites were disabled */ + note?: string [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 9fece7ebbfa..5a607b29625 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -343,6 +343,9 @@ export const schemaDict = { invitesDisabled: { type: 'boolean', }, + inviteNote: { + type: 'string', + }, }, }, repoViewDetail: { @@ -401,6 +404,9 @@ export const schemaDict = { invitesDisabled: { type: 'boolean', }, + inviteNote: { + type: 'string', + }, }, }, repoViewNotFound: { @@ -640,6 +646,11 @@ export const schemaDict = { type: 'string', format: 'did', }, + note: { + type: 'string', + description: + 'Additionally add a note describing why the invites were disabled', + }, }, }, }, @@ -694,6 +705,11 @@ export const schemaDict = { type: 'string', format: 'did', }, + note: { + type: 'string', + description: + 'Additionally add a note describing why the invites were disabled', + }, }, }, }, 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 c73be1be6e4..32edcc85ce1 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts @@ -189,6 +189,7 @@ export interface RepoView { moderation: Moderation invitedBy?: ComAtprotoServerDefs.InviteCode invitesDisabled?: boolean + inviteNote?: string [k: string]: unknown } @@ -215,6 +216,7 @@ export interface RepoViewDetail { invitedBy?: ComAtprotoServerDefs.InviteCode invites?: ComAtprotoServerDefs.InviteCode[] invitesDisabled?: boolean + inviteNote?: string [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts index fdba69e2e4b..47b3e6a2f55 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts @@ -12,6 +12,8 @@ export interface QueryParams {} export interface InputSchema { account: string + /** Additionally add a note describing why the invites were disabled */ + note?: string [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts index fdba69e2e4b..47b3e6a2f55 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts @@ -12,6 +12,8 @@ export interface QueryParams {} export interface InputSchema { account: string + /** Additionally add a note describing why the invites were disabled */ + note?: string [k: string]: unknown } diff --git a/packages/pds/src/api/com/atproto/admin/disableAccountInvites.ts b/packages/pds/src/api/com/atproto/admin/disableAccountInvites.ts index 4e6b0da2d14..9c2b64b9697 100644 --- a/packages/pds/src/api/com/atproto/admin/disableAccountInvites.ts +++ b/packages/pds/src/api/com/atproto/admin/disableAccountInvites.ts @@ -9,11 +9,11 @@ export default function (server: Server, ctx: AppContext) { if (!auth.credentials.admin) { throw new AuthRequiredError('Insufficient privileges') } - const { account } = input.body + const { account, note } = input.body await ctx.db.db .updateTable('user_account') .where('did', '=', account) - .set({ invitesDisabled: 1 }) + .set({ invitesDisabled: 1, inviteNote: note?.trim() || null }) .execute() }, }) diff --git a/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts b/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts index 8f2648c9614..1bb588368e4 100644 --- a/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts +++ b/packages/pds/src/api/com/atproto/admin/enableAccountInvites.ts @@ -9,11 +9,11 @@ export default function (server: Server, ctx: AppContext) { if (!auth.credentials.admin) { throw new AuthRequiredError('Insufficient privileges') } - const { account } = input.body + const { account, note } = input.body await ctx.db.db .updateTable('user_account') .where('did', '=', account) - .set({ invitesDisabled: 0 }) + .set({ invitesDisabled: 0, inviteNote: note?.trim() || null }) .execute() }, }) diff --git a/packages/pds/src/db/migrations/20230801T141349990Z-invite-note.ts b/packages/pds/src/db/migrations/20230801T141349990Z-invite-note.ts new file mode 100644 index 00000000000..c83a3030350 --- /dev/null +++ b/packages/pds/src/db/migrations/20230801T141349990Z-invite-note.ts @@ -0,0 +1,12 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .alterTable('user_account') + .addColumn('inviteNote', 'varchar') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.alterTable('user_account').dropColumn('inviteNote').execute() +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index b116974aa82..f2f31275760 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -56,3 +56,4 @@ export * as _20230605T235529700Z from './20230605T235529700Z-outgoing-repo-seq' export * as _20230703T044601833Z from './20230703T044601833Z-feed-and-label-indices' export * as _20230718T170914772Z from './20230718T170914772Z-sequencer-leader-sequence' export * as _20230727T172043676Z from './20230727T172043676Z-user-account-cursor-idx' +export * as _20230801T141349990Z from './20230801T141349990Z-invite-note' diff --git a/packages/pds/src/db/tables/user-account.ts b/packages/pds/src/db/tables/user-account.ts index 5a0ab6e8389..665521efc08 100644 --- a/packages/pds/src/db/tables/user-account.ts +++ b/packages/pds/src/db/tables/user-account.ts @@ -8,6 +8,7 @@ export interface UserAccount { passwordResetToken: string | null passwordResetGrantedAt: string | null invitesDisabled: Generated<0 | 1> + inviteNote: string | null } export type UserAccountEntry = Selectable diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 9fece7ebbfa..5a607b29625 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -343,6 +343,9 @@ export const schemaDict = { invitesDisabled: { type: 'boolean', }, + inviteNote: { + type: 'string', + }, }, }, repoViewDetail: { @@ -401,6 +404,9 @@ export const schemaDict = { invitesDisabled: { type: 'boolean', }, + inviteNote: { + type: 'string', + }, }, }, repoViewNotFound: { @@ -640,6 +646,11 @@ export const schemaDict = { type: 'string', format: 'did', }, + note: { + type: 'string', + description: + 'Additionally add a note describing why the invites were disabled', + }, }, }, }, @@ -694,6 +705,11 @@ export const schemaDict = { type: 'string', format: 'did', }, + note: { + type: 'string', + description: + 'Additionally add a note describing why the invites were disabled', + }, }, }, }, 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 c73be1be6e4..32edcc85ce1 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts @@ -189,6 +189,7 @@ export interface RepoView { moderation: Moderation invitedBy?: ComAtprotoServerDefs.InviteCode invitesDisabled?: boolean + inviteNote?: string [k: string]: unknown } @@ -215,6 +216,7 @@ export interface RepoViewDetail { invitedBy?: ComAtprotoServerDefs.InviteCode invites?: ComAtprotoServerDefs.InviteCode[] invitesDisabled?: boolean + inviteNote?: string [k: string]: unknown } diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts b/packages/pds/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts index fdba69e2e4b..47b3e6a2f55 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts @@ -12,6 +12,8 @@ export interface QueryParams {} export interface InputSchema { account: string + /** Additionally add a note describing why the invites were disabled */ + note?: string [k: string]: unknown } diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts b/packages/pds/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts index fdba69e2e4b..47b3e6a2f55 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts @@ -12,6 +12,8 @@ export interface QueryParams {} export interface InputSchema { account: string + /** Additionally add a note describing why the invites were disabled */ + note?: string [k: string]: unknown } diff --git a/packages/pds/src/services/moderation/views.ts b/packages/pds/src/services/moderation/views.ts index 51f434e83de..e0855cb3445 100644 --- a/packages/pds/src/services/moderation/views.ts +++ b/packages/pds/src/services/moderation/views.ts @@ -59,6 +59,7 @@ export class ModerationViews { 'did_handle.did as did', 'user_account.email as email', 'user_account.invitesDisabled as invitesDisabled', + 'user_account.inviteNote as inviteNote', 'profile_block.content as profileBytes', ]) .execute(), @@ -88,7 +89,8 @@ export class ModerationViews { ) const views = results.map((r) => { - const { email, invitesDisabled, profileBytes } = infoByDid[r.did] ?? {} + const { email, invitesDisabled, profileBytes, inviteNote } = + infoByDid[r.did] ?? {} const action = actionByDid[r.did] const relatedRecords: object[] = [] if (profileBytes) { @@ -107,6 +109,7 @@ export class ModerationViews { }, invitedBy: invitedBy[r.did], invitesDisabled: invitesDisabled === 1, + inviteNote: inviteNote ?? undefined, } }) diff --git a/packages/pds/tests/views/admin/invites.test.ts b/packages/pds/tests/views/admin/invites.test.ts index fad766e0c53..d315b7dbdd7 100644 --- a/packages/pds/tests/views/admin/invites.test.ts +++ b/packages/pds/tests/views/admin/invites.test.ts @@ -219,8 +219,9 @@ describe('pds admin invite views', () => { }) it('disables an account from getting additional invite codes', async () => { + const reasonForDisabling = 'User is selling invites' await agent.api.com.atproto.admin.disableAccountInvites( - { account: carol }, + { account: carol, note: reasonForDisabling }, { encoding: 'application/json', headers: { authorization: adminAuth() } }, ) @@ -229,6 +230,7 @@ describe('pds admin invite views', () => { { headers: { authorization: adminAuth() } }, ) expect(repoRes.data.invitesDisabled).toBe(true) + expect(repoRes.data.inviteNote).toBe(reasonForDisabling) const invRes = await agent.api.com.atproto.server.getAccountInviteCodes( {}, @@ -237,6 +239,34 @@ describe('pds admin invite views', () => { expect(invRes.data.codes.length).toBe(0) }) + it('allows setting reason when enabling and disabling invite codes', async () => { + const reasonForEnabling = 'User is confirmed they will play nice' + const reasonForDisabling = 'User is selling invites' + await agent.api.com.atproto.admin.enableAccountInvites( + { account: carol, note: reasonForEnabling }, + { encoding: 'application/json', headers: { authorization: adminAuth() } }, + ) + + const afterEnable = await agent.api.com.atproto.admin.getRepo( + { did: carol }, + { headers: { authorization: adminAuth() } }, + ) + expect(afterEnable.data.invitesDisabled).toBe(false) + expect(afterEnable.data.inviteNote).toBe(reasonForEnabling) + + await agent.api.com.atproto.admin.disableAccountInvites( + { account: carol, note: reasonForDisabling }, + { encoding: 'application/json', headers: { authorization: adminAuth() } }, + ) + + const afterDisable = await agent.api.com.atproto.admin.getRepo( + { did: carol }, + { headers: { authorization: adminAuth() } }, + ) + expect(afterDisable.data.invitesDisabled).toBe(true) + expect(afterDisable.data.inviteNote).toBe(reasonForDisabling) + }) + it('creates codes in the background but disables them', async () => { const res = await server.ctx.db.db .selectFrom('invite_code') From 2cbc4c31a52c04c450c9f98bd31770dd3ff08c02 Mon Sep 17 00:00:00 2001 From: dholms Date: Tue, 1 Aug 2023 17:57:14 -0500 Subject: [PATCH 080/237] v0.4.4 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 3ee3e2a824c..7871a04bf38 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.4.3", + "version": "0.4.4", "main": "src/index.ts", "scripts": { "codegen": "lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From 450dff7fa398b4bdf68ef618989d0ac2b78fe701 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 3 Aug 2023 10:43:30 -0500 Subject: [PATCH 081/237] Fix blocks in follow views (#1427) * fix blocks in follow views * check blocks between follow subjects & creators * test --- .../src/api/app/bsky/graph/getFollowers.ts | 10 +++++++ .../bsky/src/api/app/bsky/graph/getFollows.ts | 10 +++++++ packages/bsky/src/services/graph/index.ts | 19 +++++++++++++ packages/bsky/tests/views/blocks.test.ts | 28 +++++++++++++++++++ .../api/app/bsky/graph/getFollowers.ts | 8 +++++- .../app-view/api/app/bsky/graph/getFollows.ts | 8 +++++- .../pds/src/app-view/services/graph/index.ts | 19 +++++++++++++ packages/pds/tests/views/blocks.test.ts | 28 +++++++++++++++++++ 8 files changed, 128 insertions(+), 2 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts index 85f1dd78fbf..eb73b14d56d 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts @@ -14,6 +14,7 @@ export default function (server: Server, ctx: AppContext) { const { ref } = db.db.dynamic const actorService = services.actor(db) + const graphService = services.graph(db) const subjectRes = await actorService.getActor(actor) if (!subjectRes) { @@ -25,6 +26,15 @@ export default function (server: Server, ctx: AppContext) { .where('follow.subjectDid', '=', subjectRes.did) .innerJoin('actor as creator', 'creator.did', 'follow.creator') .where(notSoftDeletedClause(ref('creator'))) + .whereNotExists( + graphService.blockQb(requester, [ref('follow.creator')]), + ) + .whereNotExists( + graphService.blockRefQb( + ref('follow.subjectDid'), + ref('follow.creator'), + ), + ) .selectAll('creator') .select(['follow.cid as cid', 'follow.sortAt as sortAt']) diff --git a/packages/bsky/src/api/app/bsky/graph/getFollows.ts b/packages/bsky/src/api/app/bsky/graph/getFollows.ts index 3fdb45f2e47..35cb58e655b 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollows.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollows.ts @@ -14,6 +14,7 @@ export default function (server: Server, ctx: AppContext) { const { ref } = db.db.dynamic const actorService = services.actor(db) + const graphService = services.graph(db) const creatorRes = await actorService.getActor(actor) if (!creatorRes) { @@ -25,6 +26,15 @@ export default function (server: Server, ctx: AppContext) { .where('follow.creator', '=', creatorRes.did) .innerJoin('actor as subject', 'subject.did', 'follow.subjectDid') .where(notSoftDeletedClause(ref('subject'))) + .whereNotExists( + graphService.blockQb(requester, [ref('follow.subjectDid')]), + ) + .whereNotExists( + graphService.blockRefQb( + ref('follow.subjectDid'), + ref('follow.creator'), + ), + ) .selectAll('subject') .select(['follow.cid as cid', 'follow.sortAt as sortAt']) diff --git a/packages/bsky/src/services/graph/index.ts b/packages/bsky/src/services/graph/index.ts index 22e2c5b4fd5..52834a500ac 100644 --- a/packages/bsky/src/services/graph/index.ts +++ b/packages/bsky/src/services/graph/index.ts @@ -133,6 +133,25 @@ export class GraphService { .select(['creator', 'subjectDid']) } + blockRefQb(first: DbRef, second: DbRef) { + return this.db.db + .selectFrom('actor_block') + .where((outer) => + outer + .where((qb) => + qb + .whereRef('actor_block.creator', '=', first) + .whereRef('actor_block.subjectDid', '=', second), + ) + .orWhere((qb) => + qb + .whereRef('actor_block.subjectDid', '=', first) + .whereRef('actor_block.creator', '=', second), + ), + ) + .select(['creator', 'subjectDid']) + } + async getBlocks( requester: string, subjectHandleOrDid: string, diff --git a/packages/bsky/tests/views/blocks.test.ts b/packages/bsky/tests/views/blocks.test.ts index 3e605cb11f6..230a92de12e 100644 --- a/packages/bsky/tests/views/blocks.test.ts +++ b/packages/bsky/tests/views/blocks.test.ts @@ -184,6 +184,34 @@ describe('pds views with blocking', () => { expect(resDan.data.profiles[1].viewer?.blockedBy).toBe(false) }) + it('does not return block violating follows', async () => { + const resCarol = await agent.api.app.bsky.graph.getFollows( + { actor: carol }, + { headers: await network.serviceHeaders(alice) }, + ) + expect(resCarol.data.follows.some((f) => f.did === dan)).toBe(false) + + const resDan = await agent.api.app.bsky.graph.getFollows( + { actor: dan }, + { headers: await network.serviceHeaders(alice) }, + ) + expect(resDan.data.follows.some((f) => f.did === carol)).toBe(false) + }) + + it('does not return block violating followers', async () => { + const resCarol = await agent.api.app.bsky.graph.getFollowers( + { actor: carol }, + { headers: await network.serviceHeaders(alice) }, + ) + expect(resCarol.data.followers.some((f) => f.did === dan)).toBe(false) + + const resDan = await agent.api.app.bsky.graph.getFollowers( + { actor: dan }, + { headers: await network.serviceHeaders(alice) }, + ) + expect(resDan.data.followers.some((f) => f.did === carol)).toBe(false) + }) + it('does not return notifs for blocked accounts', async () => { const resCarol = await agent.api.app.bsky.notification.listNotifications( { diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts index d74a4c26f0c..6bec286398a 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts @@ -43,7 +43,13 @@ export default function (server: Server, ctx: AppContext) { ) .where(notSoftDeletedClause(ref('creator_repo'))) .whereNotExists( - graphService.blockQb(requester, [ref('follow.subjectDid')]), + graphService.blockQb(requester, [ref('follow.creator')]), + ) + .whereNotExists( + graphService.blockRefQb( + ref('follow.subjectDid'), + ref('follow.creator'), + ), ) .selectAll('creator') .select(['follow.cid as cid', 'follow.createdAt as createdAt']) diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts b/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts index 703b83556ac..5677999c4b6 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts @@ -43,7 +43,13 @@ export default function (server: Server, ctx: AppContext) { ) .where(notSoftDeletedClause(ref('subject_repo'))) .whereNotExists( - graphService.blockQb(requester, [ref('follow.creator')]), + graphService.blockQb(requester, [ref('follow.subjectDid')]), + ) + .whereNotExists( + graphService.blockRefQb( + ref('follow.subjectDid'), + ref('follow.creator'), + ), ) .selectAll('subject') .select(['follow.cid as cid', 'follow.createdAt as createdAt']) diff --git a/packages/pds/src/app-view/services/graph/index.ts b/packages/pds/src/app-view/services/graph/index.ts index ba8ac303f4c..1a3cee80da6 100644 --- a/packages/pds/src/app-view/services/graph/index.ts +++ b/packages/pds/src/app-view/services/graph/index.ts @@ -58,6 +58,25 @@ export class GraphService { .select(['creator', 'subjectDid']) } + blockRefQb(first: DbRef, second: DbRef) { + return this.db.db + .selectFrom('actor_block') + .where((outer) => + outer + .where((qb) => + qb + .whereRef('actor_block.creator', '=', first) + .whereRef('actor_block.subjectDid', '=', second), + ) + .orWhere((qb) => + qb + .whereRef('actor_block.subjectDid', '=', first) + .whereRef('actor_block.creator', '=', second), + ), + ) + .select(['creator', 'subjectDid']) + } + async getBlocks( requester: string, subjectHandleOrDid: string, diff --git a/packages/pds/tests/views/blocks.test.ts b/packages/pds/tests/views/blocks.test.ts index 56afc2a94d4..518c06181f6 100644 --- a/packages/pds/tests/views/blocks.test.ts +++ b/packages/pds/tests/views/blocks.test.ts @@ -226,6 +226,34 @@ describe('pds views with blocking', () => { expect(resDan.data.profiles[1].viewer?.blockedBy).toBe(false) }) + it('does not return block violating follows', async () => { + const resCarol = await agent.api.app.bsky.graph.getFollows( + { actor: carol }, + { headers: sc.getHeaders(alice) }, + ) + expect(resCarol.data.follows.some((f) => f.did === dan)).toBe(false) + + const resDan = await agent.api.app.bsky.graph.getFollows( + { actor: dan }, + { headers: sc.getHeaders(alice) }, + ) + expect(resDan.data.follows.some((f) => f.did === carol)).toBe(false) + }) + + it('does not return block violating followers', async () => { + const resCarol = await agent.api.app.bsky.graph.getFollowers( + { actor: carol }, + { headers: sc.getHeaders(alice) }, + ) + expect(resCarol.data.followers.some((f) => f.did === dan)).toBe(false) + + const resDan = await agent.api.app.bsky.graph.getFollowers( + { actor: dan }, + { headers: sc.getHeaders(alice) }, + ) + expect(resDan.data.followers.some((f) => f.did === carol)).toBe(false) + }) + it('does not return notifs for blocked accounts', async () => { const resCarol = await agent.api.app.bsky.notification.listNotifications( { From e5e24d510e5e97313400c567d3fdfda8c9fcc19c Mon Sep 17 00:00:00 2001 From: devin ivy Date: Thu, 3 Aug 2023 12:25:56 -0400 Subject: [PATCH 082/237] Push labels to PDS (#1423) * add lexicon for unspecced applyLabels procedure * implement label push to pds via unspecced.applyLabels * add hive retry to bsky appview * build * update applyLabels to work with raw label data * update bsky hive labeler * remove build --- .../workflows/build-and-push-bsky-aws.yaml | 1 - lexicons/app/bsky/unspecced/applyLabels.json | 23 +++ packages/api/src/client/index.ts | 13 ++ packages/api/src/client/lexicons.ts | 32 ++- .../types/app/bsky/unspecced/applyLabels.ts | 33 ++++ .../com/atproto/admin/enableAccountInvites.ts | 2 +- .../types/com/atproto/moderation/defs.ts | 2 +- packages/bsky/src/auth.ts | 10 + packages/bsky/src/indexer/config.ts | 8 + packages/bsky/src/labeler/base.ts | 28 +++ packages/bsky/src/labeler/hive.ts | 111 +++++++++-- packages/bsky/src/lexicon/index.ts | 8 + packages/bsky/src/lexicon/lexicons.ts | 32 ++- .../types/app/bsky/unspecced/applyLabels.ts | 36 ++++ .../com/atproto/admin/enableAccountInvites.ts | 2 +- .../types/com/atproto/moderation/defs.ts | 2 +- .../src/app-view/api/app/bsky/unspecced.ts | 24 ++- packages/pds/src/lexicon/index.ts | 8 + packages/pds/src/lexicon/lexicons.ts | 32 ++- .../types/app/bsky/unspecced/applyLabels.ts | 36 ++++ .../com/atproto/admin/enableAccountInvites.ts | 2 +- .../types/com/atproto/moderation/defs.ts | 2 +- .../pds/tests/labeler/apply-labels.test.ts | 187 ++++++++++++++++++ 23 files changed, 595 insertions(+), 39 deletions(-) create mode 100644 lexicons/app/bsky/unspecced/applyLabels.json create mode 100644 packages/api/src/client/types/app/bsky/unspecced/applyLabels.ts create mode 100644 packages/bsky/src/lexicon/types/app/bsky/unspecced/applyLabels.ts create mode 100644 packages/pds/src/lexicon/types/app/bsky/unspecced/applyLabels.ts create mode 100644 packages/pds/tests/labeler/apply-labels.test.ts diff --git a/.github/workflows/build-and-push-bsky-aws.yaml b/.github/workflows/build-and-push-bsky-aws.yaml index 09005d43a07..b656dec89c9 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 - - indexer-redis-setup env: REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }} USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }} diff --git a/lexicons/app/bsky/unspecced/applyLabels.json b/lexicons/app/bsky/unspecced/applyLabels.json new file mode 100644 index 00000000000..24c9e716ad5 --- /dev/null +++ b/lexicons/app/bsky/unspecced/applyLabels.json @@ -0,0 +1,23 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.applyLabels", + "defs": { + "main": { + "type": "procedure", + "description": "Allow a labeler to apply labels directly.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["labels"], + "properties": { + "labels": { + "type": "array", + "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } + } + } + } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 7a08f0ba38d..6429fc217c0 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -122,6 +122,7 @@ import * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notificatio import * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications' import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen' import * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet' +import * as AppBskyUnspeccedApplyLabels from './types/app/bsky/unspecced/applyLabels' import * as 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' @@ -241,6 +242,7 @@ export * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notificatio export * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications' export * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen' export * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet' +export * as AppBskyUnspeccedApplyLabels from './types/app/bsky/unspecced/applyLabels' export * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' export * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators' export * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton' @@ -2036,6 +2038,17 @@ export class UnspeccedNS { this._service = service } + applyLabels( + data?: AppBskyUnspeccedApplyLabels.InputSchema, + opts?: AppBskyUnspeccedApplyLabels.CallOptions, + ): Promise { + return this._service.xrpc + .call('app.bsky.unspecced.applyLabels', opts?.qp, data, opts) + .catch((e) => { + throw AppBskyUnspeccedApplyLabels.toKnownErr(e) + }) + } + getPopular( params?: AppBskyUnspeccedGetPopular.QueryParams, opts?: AppBskyUnspeccedGetPopular.CallOptions, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 5a607b29625..128330e214e 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -536,7 +536,6 @@ export const schemaDict = { }, moderation: { type: 'object', - required: [], properties: { currentAction: { type: 'ref', @@ -708,7 +707,7 @@ export const schemaDict = { note: { type: 'string', description: - 'Additionally add a note describing why the invites were disabled', + 'Additionally add a note describing why the invites were enabled', }, }, }, @@ -1668,7 +1667,7 @@ export const schemaDict = { }, reasonSexual: { type: 'token', - description: 'Unwanted or mis-labeled sexual content', + description: 'Unwanted or mislabeled sexual content', }, reasonRude: { type: 'token', @@ -6294,6 +6293,32 @@ export const schemaDict = { }, }, }, + AppBskyUnspeccedApplyLabels: { + lexicon: 1, + id: 'app.bsky.unspecced.applyLabels', + defs: { + main: { + type: 'procedure', + description: 'Allow a labeler to apply labels directly.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['labels'], + properties: { + labels: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#label', + }, + }, + }, + }, + }, + }, + }, + }, AppBskyUnspeccedGetPopular: { lexicon: 1, id: 'app.bsky.unspecced.getPopular', @@ -6561,6 +6586,7 @@ export const ids = { 'app.bsky.notification.listNotifications', AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen', AppBskyRichtextFacet: 'app.bsky.richtext.facet', + AppBskyUnspeccedApplyLabels: 'app.bsky.unspecced.applyLabels', AppBskyUnspeccedGetPopular: 'app.bsky.unspecced.getPopular', AppBskyUnspeccedGetPopularFeedGenerators: 'app.bsky.unspecced.getPopularFeedGenerators', diff --git a/packages/api/src/client/types/app/bsky/unspecced/applyLabels.ts b/packages/api/src/client/types/app/bsky/unspecced/applyLabels.ts new file mode 100644 index 00000000000..c8e72746a42 --- /dev/null +++ b/packages/api/src/client/types/app/bsky/unspecced/applyLabels.ts @@ -0,0 +1,33 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' + +export interface QueryParams {} + +export interface InputSchema { + labels: ComAtprotoLabelDefs.Label[] + [k: string]: unknown +} + +export interface CallOptions { + headers?: Headers + qp?: QueryParams + encoding: 'application/json' +} + +export interface Response { + success: boolean + headers: Headers +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/client/types/com/atproto/admin/enableAccountInvites.ts b/packages/api/src/client/types/com/atproto/admin/enableAccountInvites.ts index cf61d3026d4..e20c3f14d49 100644 --- a/packages/api/src/client/types/com/atproto/admin/enableAccountInvites.ts +++ b/packages/api/src/client/types/com/atproto/admin/enableAccountInvites.ts @@ -11,7 +11,7 @@ export interface QueryParams {} export interface InputSchema { account: string - /** Additionally add a note describing why the invites were disabled */ + /** Additionally add a note describing why the invites were enabled */ note?: string [k: string]: unknown } diff --git a/packages/api/src/client/types/com/atproto/moderation/defs.ts b/packages/api/src/client/types/com/atproto/moderation/defs.ts index c8613faf0fb..b6463993614 100644 --- a/packages/api/src/client/types/com/atproto/moderation/defs.ts +++ b/packages/api/src/client/types/com/atproto/moderation/defs.ts @@ -21,7 +21,7 @@ export const REASONSPAM = 'com.atproto.moderation.defs#reasonSpam' export const REASONVIOLATION = 'com.atproto.moderation.defs#reasonViolation' /** Misleading identity, affiliation, or content */ export const REASONMISLEADING = 'com.atproto.moderation.defs#reasonMisleading' -/** Unwanted or mis-labeled sexual content */ +/** 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' diff --git a/packages/bsky/src/auth.ts b/packages/bsky/src/auth.ts index cedf0de6b6f..7c73e9f1af7 100644 --- a/packages/bsky/src/auth.ts +++ b/packages/bsky/src/auth.ts @@ -73,6 +73,16 @@ export const parseBasicAuth = ( return { username, password } } +export const buildBasicAuth = (username: string, password: string): string => { + return ( + BASIC + + uint8arrays.toString( + uint8arrays.fromString(`${username}:${password}`, 'utf8'), + 'base64pad', + ) + ) +} + export const getJwtStrFromReq = (req: express.Request): string | null => { const { authorization = '' } = req.headers if (!authorization.startsWith(BEARER)) { diff --git a/packages/bsky/src/indexer/config.ts b/packages/bsky/src/indexer/config.ts index 04efee208e0..619c245dd0e 100644 --- a/packages/bsky/src/indexer/config.ts +++ b/packages/bsky/src/indexer/config.ts @@ -15,6 +15,7 @@ export interface IndexerConfigValues { labelerDid: string hiveApiKey?: string labelerKeywords: Record + labelerPushUrl?: string indexerConcurrency?: number indexerPartitionIds: number[] indexerPartitionBatchSize?: number @@ -54,6 +55,8 @@ export class IndexerConfig { DAY, ) const labelerDid = process.env.LABELER_DID || 'did:example:labeler' + const labelerPushUrl = + overrides?.labelerPushUrl || process.env.LABELER_PUSH_URL || undefined const hiveApiKey = process.env.HIVE_API_KEY || undefined const indexerPartitionIds = overrides?.indexerPartitionIds || @@ -84,6 +87,7 @@ export class IndexerConfig { didCacheStaleTTL, didCacheMaxTTL, labelerDid, + labelerPushUrl, hiveApiKey, indexerPartitionIds, indexerConcurrency, @@ -139,6 +143,10 @@ export class IndexerConfig { return this.cfg.labelerDid } + get labelerPushUrl() { + return this.cfg.labelerPushUrl + } + get hiveApiKey() { return this.cfg.hiveApiKey } diff --git a/packages/bsky/src/labeler/base.ts b/packages/bsky/src/labeler/base.ts index ddb4f25889e..8253ba9a7a2 100644 --- a/packages/bsky/src/labeler/base.ts +++ b/packages/bsky/src/labeler/base.ts @@ -1,5 +1,6 @@ import stream from 'stream' import { AtUri } from '@atproto/uri' +import { AtpAgent } from '@atproto/api' import { cidForRecord } from '@atproto/repo' import { dedupe, getFieldsFromRecord } from './util' import { labelerLogger as log } from '../logger' @@ -8,9 +9,11 @@ import Database from '../db' import { IdResolver } from '@atproto/identity' import { BackgroundQueue } from '../background' import { IndexerConfig } from '../indexer/config' +import { buildBasicAuth } from '../auth' export abstract class Labeler { public backgroundQueue: BackgroundQueue + public pushAgent?: AtpAgent constructor( protected ctx: { db: Database @@ -20,6 +23,14 @@ export abstract class Labeler { }, ) { this.backgroundQueue = ctx.backgroundQueue + if (ctx.cfg.labelerPushUrl) { + const url = new URL(ctx.cfg.labelerPushUrl) + this.pushAgent = new AtpAgent({ service: url.origin }) + this.pushAgent.api.setHeader( + 'authorization', + buildBasicAuth(url.username, url.password), + ) + } } processRecord(uri: AtUri, obj: unknown) { @@ -51,6 +62,23 @@ export abstract class Labeler { .values(rows) .onConflict((oc) => oc.doNothing()) .execute() + + if (this.pushAgent) { + const agent = this.pushAgent + try { + await agent.api.app.bsky.unspecced.applyLabels({ labels: rows }) + } catch (err) { + log.error( + { + err, + uri: uri.toString(), + labels, + receiver: agent.service.toString(), + }, + 'failed to push labels', + ) + } + } } async labelRecord(uri: AtUri, obj: unknown): Promise { diff --git a/packages/bsky/src/labeler/hive.ts b/packages/bsky/src/labeler/hive.ts index dfa9ed132bb..42bf70dcc13 100644 --- a/packages/bsky/src/labeler/hive.ts +++ b/packages/bsky/src/labeler/hive.ts @@ -7,6 +7,7 @@ import { IdResolver } from '@atproto/identity' import Database from '../db' import { BackgroundQueue } from '../background' import { IndexerConfig } from '../indexer/config' +import { retryHttp } from '../util/retry' const HIVE_ENDPOINT = 'https://api.thehive.ai/api/v2/task/sync' @@ -51,13 +52,16 @@ export const makeHiveReq = async ( ): Promise => { const form = new FormData() form.append('media', blob) - const res = await axios.post(HIVE_ENDPOINT, form, { - headers: { - 'Content-Type': 'multipart/form-data', - authorization: `token ${hiveApiKey}`, - accept: 'application/json', - }, - }) + + const res = await retryHttp(() => + axios.post(HIVE_ENDPOINT, form, { + headers: { + 'Content-Type': 'multipart/form-data', + authorization: `token ${hiveApiKey}`, + accept: 'application/json', + }, + }), + ) return respToClasses(res.data) } @@ -73,30 +77,101 @@ export const respToClasses = (res: HiveResp): HiveRespClass[] => { return classes } -// sexual: https://docs.thehive.ai/docs/sexual-content +// Matches only one (or none) of: porn, sexual, nudity +// +// porn: sexual and nudity. including both explicit activity or full-frontal and suggestive/intent +// sexual: sexually suggestive, not explicit; may include some forms of nudity +// nudity: non-sexual nudity (eg, artistic, possibly some photographic) +// +// hive docs/definitions: https://docs.thehive.ai/docs/sexual-content +export const sexualLabels = (classes: HiveRespClass[]): string[] => { + const scores = {} + + for (const cls of classes) { + scores[cls.class] = cls.score + } + + // first check if porn... + for (const pornClass of [ + 'yes_sexual_activity', + 'animal_genitalia_and_human', + 'yes_realistic_nsfw', + ]) { + if (scores[pornClass] >= 0.9) { + return ['porn'] + } + } + if (scores['general_nsfw'] >= 0.9) { + // special case for some anime examples + if (scores['animated_animal_genitalia'] >= 0.5) { + return ['porn'] + } + // special case for some pornographic/explicit classic drawings + if (scores['yes_undressed'] >= 0.9 && scores['yes_sexual_activity'] > 0.9) { + return ['porn'] + } + } + + // then check for sexual suggestive (which may include nudity)... + for (const sexualClass of ['yes_sexual_intent', 'yes_sex_toy']) { + if (scores[sexualClass] >= 0.9) { + return ['sexual'] + } + } + if (scores['yes_undressed'] >= 0.9) { + // special case for bondage examples + if (scores['yes_sex_toy'] > 0.75) { + return ['sexual'] + } + } + + // then non-sexual nudity... + for (const nudityClass of [ + 'yes_male_nudity', + 'yes_female_nudity', + 'yes_undressed', + ]) { + if (scores[nudityClass] >= 0.9) { + return ['nudity'] + } + } + + // then finally flag remaining "underwear" images in to sexually suggestive + // (after non-sexual content already labeled above) + for (const nudityClass of ['yes_male_underwear', 'yes_female_underwear']) { + if (scores[nudityClass] >= 0.9) { + // TODO: retaining 'underwear' label for a short time to help understand + // the impact of labeling all "underwear" as "sexual". This *will* be + // pulling in somewhat non-sexual content in to "sexual" label. + return ['sexual', 'underwear'] + } + } + + return [] +} + // gore and violence: https://docs.thehive.ai/docs/class-descriptions-violence-gore -// iconography: https://docs.thehive.ai/docs/class-descriptions-hate-bullying const labelForClass = { - yes_sexual_activity: 'porn', - animal_genitalia_and_human: 'porn', // for some reason not included in 'yes_sexual_activity' - yes_male_nudity: 'nudity', - yes_female_nudity: 'nudity', - general_suggestive: 'sexual', very_bloody: 'gore', human_corpse: 'corpse', + hanging: 'corpse', +} +const labelForClassLessSensitive = { yes_self_harm: 'self-harm', - yes_nazi: 'icon-nazi', - yes_kkk: 'icon-kkk', - yes_confederate: 'icon-confederate', } export const summarizeLabels = (classes: HiveRespClass[]): string[] => { - const labels: string[] = [] + const labels: string[] = sexualLabels(classes) for (const cls of classes) { if (labelForClass[cls.class] && cls.score >= 0.9) { labels.push(labelForClass[cls.class]) } } + for (const cls of classes) { + if (labelForClassLessSensitive[cls.class] && cls.score >= 0.96) { + labels.push(labelForClassLessSensitive[cls.class]) + } + } return labels } diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 0394591ca06..17c90d9ec8d 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -102,6 +102,7 @@ import * as AppBskyGraphUnmuteActorList from './types/app/bsky/graph/unmuteActor import * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount' import * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications' import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen' +import * as AppBskyUnspeccedApplyLabels from './types/app/bsky/unspecced/applyLabels' import * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' import * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators' import * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton' @@ -1041,6 +1042,13 @@ export class UnspeccedNS { this._server = server } + applyLabels( + cfg: ConfigOf>>, + ) { + const nsid = 'app.bsky.unspecced.applyLabels' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + getPopular( cfg: ConfigOf>>, ) { diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 5a607b29625..128330e214e 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -536,7 +536,6 @@ export const schemaDict = { }, moderation: { type: 'object', - required: [], properties: { currentAction: { type: 'ref', @@ -708,7 +707,7 @@ export const schemaDict = { note: { type: 'string', description: - 'Additionally add a note describing why the invites were disabled', + 'Additionally add a note describing why the invites were enabled', }, }, }, @@ -1668,7 +1667,7 @@ export const schemaDict = { }, reasonSexual: { type: 'token', - description: 'Unwanted or mis-labeled sexual content', + description: 'Unwanted or mislabeled sexual content', }, reasonRude: { type: 'token', @@ -6294,6 +6293,32 @@ export const schemaDict = { }, }, }, + AppBskyUnspeccedApplyLabels: { + lexicon: 1, + id: 'app.bsky.unspecced.applyLabels', + defs: { + main: { + type: 'procedure', + description: 'Allow a labeler to apply labels directly.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['labels'], + properties: { + labels: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#label', + }, + }, + }, + }, + }, + }, + }, + }, AppBskyUnspeccedGetPopular: { lexicon: 1, id: 'app.bsky.unspecced.getPopular', @@ -6561,6 +6586,7 @@ export const ids = { 'app.bsky.notification.listNotifications', AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen', AppBskyRichtextFacet: 'app.bsky.richtext.facet', + AppBskyUnspeccedApplyLabels: 'app.bsky.unspecced.applyLabels', AppBskyUnspeccedGetPopular: 'app.bsky.unspecced.getPopular', AppBskyUnspeccedGetPopularFeedGenerators: 'app.bsky.unspecced.getPopularFeedGenerators', diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/applyLabels.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/applyLabels.ts new file mode 100644 index 00000000000..1c07d41d040 --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/applyLabels.ts @@ -0,0 +1,36 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' + +export interface QueryParams {} + +export interface InputSchema { + labels: ComAtprotoLabelDefs.Label[] + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type Handler = (ctx: { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +}) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts index 47b3e6a2f55..7f437adfb44 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts @@ -12,7 +12,7 @@ export interface QueryParams {} export interface InputSchema { account: string - /** Additionally add a note describing why the invites were disabled */ + /** Additionally add a note describing why the invites were enabled */ note?: string [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/com/atproto/moderation/defs.ts b/packages/bsky/src/lexicon/types/com/atproto/moderation/defs.ts index 9ab6e60f9bd..81697226189 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/moderation/defs.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/moderation/defs.ts @@ -21,7 +21,7 @@ export const REASONSPAM = 'com.atproto.moderation.defs#reasonSpam' export const REASONVIOLATION = 'com.atproto.moderation.defs#reasonViolation' /** Misleading identity, affiliation, or content */ export const REASONMISLEADING = 'com.atproto.moderation.defs#reasonMisleading' -/** Unwanted or mis-labeled sexual content */ +/** 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' diff --git a/packages/pds/src/app-view/api/app/bsky/unspecced.ts b/packages/pds/src/app-view/api/app/bsky/unspecced.ts index ab4f22649a9..3a281fb4a14 100644 --- a/packages/pds/src/app-view/api/app/bsky/unspecced.ts +++ b/packages/pds/src/app-view/api/app/bsky/unspecced.ts @@ -1,14 +1,16 @@ +import { NotEmptyArray } from '@atproto/common' +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import { FeedKeyset } from './util/feed' import { GenericKeyset, paginate } from '../../../../db/pagination' import AppContext from '../../../../context' import { FeedRow } from '../../../services/feed' -import { isPostView } from '../../../../lexicon/types/app/bsky/feed/defs' -import { NotEmptyArray } from '@atproto/common' +import { + isPostView, + GeneratorView, +} from '../../../../lexicon/types/app/bsky/feed/defs' import { isViewRecord } from '../../../../lexicon/types/app/bsky/embed/record' import { countAll, valuesList } from '../../../../db/util' -import { InvalidRequestError } from '@atproto/xrpc-server' -import { GeneratorView } from '@atproto/api/src/client/types/app/bsky/feed/defs' +import { FeedKeyset } from './util/feed' const NO_WHATS_HOT_LABELS: NotEmptyArray = [ '!no-promote', @@ -189,6 +191,18 @@ export default function (server: Server, ctx: AppContext) { } }, }) + + server.app.bsky.unspecced.applyLabels({ + auth: ctx.roleVerifier, + handler: async ({ auth, input }) => { + if (!auth.credentials.admin) { + throw new AuthRequiredError('Insufficient privileges') + } + const { services, db } = ctx + const { labels } = input.body + await services.appView.label(db).createLabels(labels) + }, + }) } type Result = { likeCount: number; cid: string } diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 0394591ca06..17c90d9ec8d 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -102,6 +102,7 @@ import * as AppBskyGraphUnmuteActorList from './types/app/bsky/graph/unmuteActor import * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount' import * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications' import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen' +import * as AppBskyUnspeccedApplyLabels from './types/app/bsky/unspecced/applyLabels' import * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' import * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators' import * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton' @@ -1041,6 +1042,13 @@ export class UnspeccedNS { this._server = server } + applyLabels( + cfg: ConfigOf>>, + ) { + const nsid = 'app.bsky.unspecced.applyLabels' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + getPopular( cfg: ConfigOf>>, ) { diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 5a607b29625..128330e214e 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -536,7 +536,6 @@ export const schemaDict = { }, moderation: { type: 'object', - required: [], properties: { currentAction: { type: 'ref', @@ -708,7 +707,7 @@ export const schemaDict = { note: { type: 'string', description: - 'Additionally add a note describing why the invites were disabled', + 'Additionally add a note describing why the invites were enabled', }, }, }, @@ -1668,7 +1667,7 @@ export const schemaDict = { }, reasonSexual: { type: 'token', - description: 'Unwanted or mis-labeled sexual content', + description: 'Unwanted or mislabeled sexual content', }, reasonRude: { type: 'token', @@ -6294,6 +6293,32 @@ export const schemaDict = { }, }, }, + AppBskyUnspeccedApplyLabels: { + lexicon: 1, + id: 'app.bsky.unspecced.applyLabels', + defs: { + main: { + type: 'procedure', + description: 'Allow a labeler to apply labels directly.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['labels'], + properties: { + labels: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:com.atproto.label.defs#label', + }, + }, + }, + }, + }, + }, + }, + }, AppBskyUnspeccedGetPopular: { lexicon: 1, id: 'app.bsky.unspecced.getPopular', @@ -6561,6 +6586,7 @@ export const ids = { 'app.bsky.notification.listNotifications', AppBskyNotificationUpdateSeen: 'app.bsky.notification.updateSeen', AppBskyRichtextFacet: 'app.bsky.richtext.facet', + AppBskyUnspeccedApplyLabels: 'app.bsky.unspecced.applyLabels', AppBskyUnspeccedGetPopular: 'app.bsky.unspecced.getPopular', AppBskyUnspeccedGetPopularFeedGenerators: 'app.bsky.unspecced.getPopularFeedGenerators', diff --git a/packages/pds/src/lexicon/types/app/bsky/unspecced/applyLabels.ts b/packages/pds/src/lexicon/types/app/bsky/unspecced/applyLabels.ts new file mode 100644 index 00000000000..1c07d41d040 --- /dev/null +++ b/packages/pds/src/lexicon/types/app/bsky/unspecced/applyLabels.ts @@ -0,0 +1,36 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import express from 'express' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { lexicons } from '../../../../lexicons' +import { isObj, hasProp } from '../../../../util' +import { CID } from 'multiformats/cid' +import { HandlerAuth } from '@atproto/xrpc-server' +import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' + +export interface QueryParams {} + +export interface InputSchema { + labels: ComAtprotoLabelDefs.Label[] + [k: string]: unknown +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | void +export type Handler = (ctx: { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +}) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts b/packages/pds/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts index 47b3e6a2f55..7f437adfb44 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts @@ -12,7 +12,7 @@ export interface QueryParams {} export interface InputSchema { account: string - /** Additionally add a note describing why the invites were disabled */ + /** Additionally add a note describing why the invites were enabled */ note?: string [k: string]: unknown } diff --git a/packages/pds/src/lexicon/types/com/atproto/moderation/defs.ts b/packages/pds/src/lexicon/types/com/atproto/moderation/defs.ts index 9ab6e60f9bd..81697226189 100644 --- a/packages/pds/src/lexicon/types/com/atproto/moderation/defs.ts +++ b/packages/pds/src/lexicon/types/com/atproto/moderation/defs.ts @@ -21,7 +21,7 @@ export const REASONSPAM = 'com.atproto.moderation.defs#reasonSpam' export const REASONVIOLATION = 'com.atproto.moderation.defs#reasonViolation' /** Misleading identity, affiliation, or content */ export const REASONMISLEADING = 'com.atproto.moderation.defs#reasonMisleading' -/** Unwanted or mis-labeled sexual content */ +/** 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' diff --git a/packages/pds/tests/labeler/apply-labels.test.ts b/packages/pds/tests/labeler/apply-labels.test.ts new file mode 100644 index 00000000000..d4a4255df13 --- /dev/null +++ b/packages/pds/tests/labeler/apply-labels.test.ts @@ -0,0 +1,187 @@ +import AtpAgent from '@atproto/api' +import { + adminAuth, + CloseFn, + moderatorAuth, + runTestServer, + TestServerInfo, +} from '../_util' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' + +describe('unspecced.applyLabels', () => { + let server: TestServerInfo + let close: CloseFn + let agent: AtpAgent + let sc: SeedClient + + beforeAll(async () => { + server = await runTestServer({ + dbPostgresSchema: 'moderation', + }) + close = server.close + agent = new AtpAgent({ service: server.url }) + sc = new SeedClient(agent) + await basicSeed(sc) + }) + + afterAll(async () => { + await close() + }) + + it('requires admin auth.', async () => { + const tryToLabel = agent.api.app.bsky.unspecced.applyLabels( + { + labels: [ + { + src: server.ctx.cfg.labelerDid, + uri: sc.dids.carol, + val: 'cats', + neg: false, + cts: new Date().toISOString(), + }, + ], + }, + { + encoding: 'application/json', + headers: { authorization: moderatorAuth() }, + }, + ) + await expect(tryToLabel).rejects.toThrow('Insufficient privileges') + }) + + it('adds and removes labels on record as though applied by the labeler.', async () => { + const post = sc.posts[sc.dids.bob][1].ref + await agent.api.app.bsky.unspecced.applyLabels( + { + labels: [ + { + src: server.ctx.cfg.labelerDid, + uri: post.uriStr, + cid: post.cidStr, + val: 'birds', + neg: false, + cts: new Date().toISOString(), + }, + { + src: server.ctx.cfg.labelerDid, + uri: post.uriStr, + cid: post.cidStr, + val: 'bats', + neg: false, + cts: new Date().toISOString(), + }, + ], + }, + { + encoding: 'application/json', + headers: { authorization: adminAuth() }, + }, + ) + await expect(getRecordLabels(post.uriStr)).resolves.toEqual([ + 'birds', + 'bats', + ]) + await agent.api.app.bsky.unspecced.applyLabels( + { + labels: [ + { + src: server.ctx.cfg.labelerDid, + uri: post.uriStr, + cid: post.cidStr, + val: 'birds', + neg: true, + cts: new Date().toISOString(), + }, + { + src: server.ctx.cfg.labelerDid, + uri: post.uriStr, + cid: post.cidStr, + val: 'bats', + neg: true, + cts: new Date().toISOString(), + }, + ], + }, + { + encoding: 'application/json', + headers: { authorization: adminAuth() }, + }, + ) + await expect(getRecordLabels(post.uriStr)).resolves.toEqual([]) + }) + + it('adds and removes labels on repo as though applied by the labeler.', async () => { + await agent.api.app.bsky.unspecced.applyLabels( + { + labels: [ + { + src: server.ctx.cfg.labelerDid, + uri: sc.dids.carol, + val: 'birds', + neg: false, + cts: new Date().toISOString(), + }, + { + src: server.ctx.cfg.labelerDid, + uri: sc.dids.carol, + val: 'bats', + neg: false, + cts: new Date().toISOString(), + }, + ], + }, + { + encoding: 'application/json', + headers: { authorization: adminAuth() }, + }, + ) + await expect(getRepoLabels(sc.dids.carol)).resolves.toEqual([ + 'birds', + 'bats', + ]) + await agent.api.app.bsky.unspecced.applyLabels( + { + labels: [ + { + src: server.ctx.cfg.labelerDid, + uri: sc.dids.carol, + val: 'birds', + neg: true, + cts: new Date().toISOString(), + }, + { + src: server.ctx.cfg.labelerDid, + uri: sc.dids.carol, + val: 'bats', + neg: true, + cts: new Date().toISOString(), + }, + ], + }, + { + encoding: 'application/json', + headers: { authorization: adminAuth() }, + }, + ) + await expect(getRepoLabels(sc.dids.carol)).resolves.toEqual([]) + }) + + async function getRecordLabels(uri: string) { + const result = await agent.api.com.atproto.admin.getRecord( + { uri }, + { headers: { authorization: adminAuth() } }, + ) + const labels = result.data.labels ?? [] + return labels.map((l) => l.val) + } + + async function getRepoLabels(did: string) { + const result = await agent.api.com.atproto.admin.getRepo( + { did }, + { headers: { authorization: adminAuth() } }, + ) + const labels = result.data.labels ?? [] + return labels.map((l) => l.val) + } +}) From b30f847d095b1f5bc7d91b635420fee0c01c3045 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 3 Aug 2023 10:28:06 -0700 Subject: [PATCH 083/237] Labeling & moderation updates to SDK (#1366) * First pass on label definitions and reference-doc generation for them * Tune labels * Drop the 'csam' label in favor of using !hide CSAM is an extremely delicate issue. Moderators must be careful to remove it from the system without calling direct attention to it. Using the generic !hide achieves the same effect while obscuring the reason for the removal. Server and AppView bans will then be employed (along with any other needed mechanisms) to strip the content from the network. There is also a 'false accusation' concern with using the csam label. Fingerprinting techniques and AI are used to scan for csam. These are capable of producing false positives which will then be reverted after human review. The reputational damage caused by a false positive could be severe, so we want to be careful about any system which might publicize a false positive. * Add label definition code generation * Implement all type signatures for moderation * Complete a first pass on the labeling sdk * Add post-moderation test suite * Add post moderation behavior documentation * Add self-post behaviors * Improve post moderation doc * Tune up the post moderation doc * Tune up the post moderation doc * Tune up the post moderation doc * Simplify the post moderation behavior descriptions * More behavior description tuneup * Add profile moderation final behaviors and tests * Improve generated post-moderation-behaviors doc * Add profile moderation behaviors doc * Test muted-by-list specifically * Fixes to label descriptions * Fix to muted-by-list behavior * Dont blur account in moderateProfile() when muting * Tune label copy * Apply post embed blurring when account is marked with blurmedia label * Fix output signature * Fixes to blocking behavior * Rename LabelDefinitionPreference to LabelPreference * Update docs * Fix test * Fix: self-harm should blur media only * Fixes to tests * Improve label copy * Remove all labels that do not have a specific policy Communicating moderation policies with active users is important. This PR originally included labels which were proposed but did not yet have policies. While we didn't intend to use them until a policy was established, I decided it's better to hold off putting them in the code until we're sure about them. They can be found in backup files prefixed with "proposed-". --- packages/api/README.md | 81 + packages/api/definitions/labels.json | 212 ++ .../definitions/locale/en/label-groups.json | 38 + .../api/definitions/locale/en/labels.json | 366 ++++ .../locale/en/proposed-label-groups.json | 38 + .../locale/en/proposed-labels.json | 632 ++++++ .../api/definitions/moderation-behaviors.d.ts | 48 + .../post-moderation-behaviors.json | 879 ++++++++ .../profile-moderation-behaviors.json | 447 ++++ packages/api/definitions/proposed-labels.json | 326 +++ packages/api/docs/labels.md | 522 +++++ .../api/docs/moderation-behaviors/posts.md | 1919 +++++++++++++++++ .../api/docs/moderation-behaviors/profiles.md | 907 ++++++++ packages/api/docs/moderation.md | 144 ++ packages/api/package.json | 6 +- packages/api/scripts/code/label-groups.mjs | 68 + packages/api/scripts/code/labels.mjs | 68 + packages/api/scripts/docs/labels.mjs | 164 ++ .../docs/post-moderation-behaviors.mjs | 122 ++ .../docs/profile-moderation-behaviors.mjs | 122 ++ packages/api/scripts/generate-code.mjs | 4 + packages/api/scripts/generate-docs.mjs | 5 + packages/api/src/index.ts | 4 + packages/api/src/moderation/accumulator.ts | 181 ++ .../api/src/moderation/const/label-groups.ts | 143 ++ packages/api/src/moderation/const/labels.ts | 798 +++++++ packages/api/src/moderation/index.ts | 343 +++ .../api/src/moderation/subjects/account.ts | 40 + .../src/moderation/subjects/feed-generator.ts | 13 + packages/api/src/moderation/subjects/post.ts | 23 + .../api/src/moderation/subjects/profile.ts | 31 + .../src/moderation/subjects/quoted-post.ts | 62 + .../api/src/moderation/subjects/user-list.ts | 13 + packages/api/src/moderation/types.ts | 141 ++ packages/api/src/moderation/util.ts | 98 + packages/api/tests/_util.ts | 26 - packages/api/tests/post-moderation.test.ts | 46 + packages/api/tests/profile-moderation.test.ts | 46 + packages/api/tests/util/index.ts | 176 ++ .../api/tests/util/moderation-behavior.ts | 180 ++ yarn.lock | 5 + 41 files changed, 9459 insertions(+), 28 deletions(-) create mode 100644 packages/api/definitions/labels.json create mode 100644 packages/api/definitions/locale/en/label-groups.json create mode 100644 packages/api/definitions/locale/en/labels.json create mode 100644 packages/api/definitions/locale/en/proposed-label-groups.json create mode 100644 packages/api/definitions/locale/en/proposed-labels.json create mode 100644 packages/api/definitions/moderation-behaviors.d.ts create mode 100644 packages/api/definitions/post-moderation-behaviors.json create mode 100644 packages/api/definitions/profile-moderation-behaviors.json create mode 100644 packages/api/definitions/proposed-labels.json create mode 100644 packages/api/docs/labels.md create mode 100644 packages/api/docs/moderation-behaviors/posts.md create mode 100644 packages/api/docs/moderation-behaviors/profiles.md create mode 100644 packages/api/docs/moderation.md create mode 100644 packages/api/scripts/code/label-groups.mjs create mode 100644 packages/api/scripts/code/labels.mjs create mode 100644 packages/api/scripts/docs/labels.mjs create mode 100644 packages/api/scripts/docs/post-moderation-behaviors.mjs create mode 100644 packages/api/scripts/docs/profile-moderation-behaviors.mjs create mode 100644 packages/api/scripts/generate-code.mjs create mode 100644 packages/api/scripts/generate-docs.mjs create mode 100644 packages/api/src/moderation/accumulator.ts create mode 100644 packages/api/src/moderation/const/label-groups.ts create mode 100644 packages/api/src/moderation/const/labels.ts create mode 100644 packages/api/src/moderation/index.ts create mode 100644 packages/api/src/moderation/subjects/account.ts create mode 100644 packages/api/src/moderation/subjects/feed-generator.ts create mode 100644 packages/api/src/moderation/subjects/post.ts create mode 100644 packages/api/src/moderation/subjects/profile.ts create mode 100644 packages/api/src/moderation/subjects/quoted-post.ts create mode 100644 packages/api/src/moderation/subjects/user-list.ts create mode 100644 packages/api/src/moderation/types.ts create mode 100644 packages/api/src/moderation/util.ts delete mode 100644 packages/api/tests/_util.ts create mode 100644 packages/api/tests/post-moderation.test.ts create mode 100644 packages/api/tests/profile-moderation.test.ts create mode 100644 packages/api/tests/util/index.ts create mode 100644 packages/api/tests/util/moderation-behavior.ts diff --git a/packages/api/README.md b/packages/api/README.md index 62fa8db924f..8803f78e9aa 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -160,6 +160,87 @@ console.log(rt3.length) // => 25 console.log(rt3.graphemeLength) // => 1 ``` +### Moderation + +Applying the moderation system is a challenging task, but we've done our best to simplify it for you. The Moderation API helps handle a wide range of tasks, including: + +- User muting (including mutelists) +- User blocking +- Moderator labeling + +For more information, see the [Moderation Documentation](./docs/moderation.md) or the associated [Labels Reference](./docs/labels.md). + +```typescript +import {moderatePost, moderateProfile} from '@atproto/api' + +// We call the appropriate moderation function for the content +// = + +const postMod = moderatePost(postView, getOpts()) +const profileMod = moderateProfile(profileView, getOpts()) + +// We then use the output to decide how to affect rendering +// = + +if (postMod.content.filter) { + // dont render in feeds or similar + // in contexts where this is disruptive (eg threads) you should ignore this and instead check blur +} +if (postMod.content.blur) { + // render the whole object behind a cover (use postMod.content.cause to explain) + if (postMod.content.noOverride) { + // do not allow the cover the be removed + } +} +if (postMod.content.alert) { + // render a warning on the content (use postMod.content.cause to explain) +} +if (postMod.embed.blur) { + // render the embedded media behind a cover (use postMod.embed.cause to explain) + if (postMod.embed.noOverride) { + // do not allow the cover the be removed + } +} +if (postMod.embed.alert) { + // render a warning on the embedded media (use postMod.embed.cause to explain) +} +if (postMod.avatar.blur) { + // render the avatar behind a cover +} +if (postMod.avatar.alert) { + // render an alert on the avatar +} + +// The options passed into `apply()` supply the user's preferences +// = + +function getOpts() { + return { + // the logged-in user's DID + userDid: 'did:plc:1234...', + + // is adult content allowed? + adultContentEnabled: true, + + // the user's labeler settings + labelerSettings: [ + { + labeler: { + did: '...', + displayName: 'My mod service' + }, + settings: { + porn: 'hide', + sexual: 'warn', + nudity: 'ignore', + // ... + } + } + ] + } +} +``` + ## Advanced ### Advanced API calls diff --git a/packages/api/definitions/labels.json b/packages/api/definitions/labels.json new file mode 100644 index 00000000000..d8652795464 --- /dev/null +++ b/packages/api/definitions/labels.json @@ -0,0 +1,212 @@ +[ + { + "id": "system", + "configurable": false, + "labels": [ + { + "id": "!hide", + "preferences": ["hide"], + "flags": ["no-override"], + "onwarn": "blur" + }, + { + "id": "!no-promote", + "preferences": ["hide"], + "flags": [], + "onwarn": null + }, + { + "id": "!warn", + "preferences": ["warn"], + "flags": [], + "onwarn": "blur" + } + ] + }, + { + "id": "legal", + "configurable": false, + "labels": [ + { + "id": "dmca-violation", + "preferences": ["hide"], + "flags": ["no-override"], + "onwarn": "blur" + }, + { + "id": "doxxing", + "preferences": ["hide"], + "flags": ["no-override"], + "onwarn": "blur" + } + ] + }, + { + "id": "sexual", + "configurable": true, + "labels": [ + { + "id": "porn", + "preferences": ["ignore", "warn", "hide"], + "flags": ["adult"], + "onwarn": "blur-media" + }, + { + "id": "sexual", + "preferences": ["ignore", "warn", "hide"], + "flags": ["adult"], + "onwarn": "blur-media" + }, + { + "id": "nudity", + "preferences": ["ignore", "warn", "hide"], + "flags": ["adult"], + "onwarn": "blur-media" + } + ] + }, + { + "id": "violence", + "configurable": true, + "labels": [ + { + "id": "nsfl", + "preferences": ["ignore", "warn", "hide"], + "flags": ["adult"], + "onwarn": "blur-media" + }, + { + "id": "corpse", + "preferences": ["ignore", "warn", "hide"], + "flags": ["adult"], + "onwarn": "blur-media" + }, + { + "id": "gore", + "preferences": ["ignore", "warn", "hide"], + "flags": ["adult"], + "onwarn": "blur-media" + }, + { + "id": "torture", + "preferences": ["ignore", "warn", "hide"], + "flags": ["adult"], + "onwarn": "blur" + }, + { + "id": "self-harm", + "preferences": ["ignore", "warn", "hide"], + "flags": ["adult"], + "onwarn": "blur-media" + } + ] + }, + { + "id": "intolerance", + "configurable": true, + "labels": [ + { + "id": "intolerant-race", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "intolerant-gender", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "intolerant-sexual-orientation", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "intolerant-religion", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "intolerant", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "icon-intolerant", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur-media" + } + ] + }, + { + "id": "rude", + "configurable": true, + "labels": [ + { + "id": "threat", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + } + ] + }, + { + "id": "curation", + "configurable": true, + "labels": [ + { + "id": "spoiler", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + } + ] + }, + { + "id": "spam", + "configurable": true, + "labels": [ + { + "id": "spam", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + } + ] + }, + { + "id": "misinfo", + "configurable": true, + "labels": [ + { + "id": "account-security", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "net-abuse", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "impersonation", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "alert" + }, + { + "id": "scam", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "alert" + } + ] + } +] \ No newline at end of file diff --git a/packages/api/definitions/locale/en/label-groups.json b/packages/api/definitions/locale/en/label-groups.json new file mode 100644 index 00000000000..5658f0cedea --- /dev/null +++ b/packages/api/definitions/locale/en/label-groups.json @@ -0,0 +1,38 @@ +{ + "system": { + "name": "System", + "description": "Moderator overrides for special cases." + }, + "legal": { + "name": "Legal", + "description": "Content removed for legal reasons." + }, + "sexual": { + "name": "Adult Content", + "description": "Content which is sexual in nature." + }, + "violence": { + "name": "Violence", + "description": "Content which is violent or deeply disturbing." + }, + "intolerance": { + "name": "Intolerance", + "description": "Content or behavior which is hateful or intolerant toward a group of people." + }, + "rude": { + "name": "Rude", + "description": "Behavior which is rude toward other users." + }, + "curation": { + "name": "Curational", + "description": "Subjective moderation geared towards curating a more positive environment." + }, + "spam": { + "name": "Spam", + "description": "Content which doesn't add to the conversation." + }, + "misinfo": { + "name": "Misinformation", + "description": "Content which misleads or defrauds users." + } +} \ No newline at end of file diff --git a/packages/api/definitions/locale/en/labels.json b/packages/api/definitions/locale/en/labels.json new file mode 100644 index 00000000000..f4832212e52 --- /dev/null +++ b/packages/api/definitions/locale/en/labels.json @@ -0,0 +1,366 @@ +{ + "!hide": { + "settings": { + "name": "Moderator Hide", + "description": "Moderator has chosen to hide the content." + }, + "account": { + "name": "Content Blocked", + "description": "This account has been hidden by the moderators." + }, + "content": { + "name": "Content Blocked", + "description": "This content has been hidden by the moderators." + } + }, + "!no-promote": { + "settings": { + "name": "Moderator Filter", + "description": "Moderator has chosen to filter the content from feeds." + }, + "account": { + "name": "N/A", + "description": "N/A" + }, + "content": { + "name": "N/A", + "description": "N/A" + } + }, + "!warn": { + "settings": { + "name": "Moderator Warn", + "description": "Moderator has chosen to set a general warning on the content." + }, + "account": { + "name": "Content Warning", + "description": "This account has received a general warning from moderators." + }, + "content": { + "name": "Content Warning", + "description": "This content has received a general warning from moderators." + } + }, + "dmca-violation": { + "settings": { + "name": "Copyright Violation", + "description": "The content has received a DMCA takedown request." + }, + "account": { + "name": "Copyright Violation", + "description": "This account has received a DMCA takedown request. It will be restored if the concerns can be resolved." + }, + "content": { + "name": "Copyright Violation", + "description": "This content has received a DMCA takedown request. It will be restored if the concerns can be resolved." + } + }, + "doxxing": { + "settings": { + "name": "Doxxing", + "description": "Information that reveals private information about someone which has been shared without the consent of the subject." + }, + "account": { + "name": "Doxxing", + "description": "This account has been reported to publish private information about someone without their consent. This report is currently under review." + }, + "content": { + "name": "Doxxing", + "description": "This content has been reported to include private information about someone without their consent." + } + }, + "porn": { + "settings": { + "name": "Pornography", + "description": "Images of full-frontal nudity (genitalia) in any sexualized context, or explicit sexual activity (meaning contact with genitalia or breasts) even if partially covered. Includes graphic sexual cartoons (often jokes/memes)." + }, + "account": { + "name": "Pornography", + "description": "This account contains imagery of full-frontal nudity or explicit sexual activity." + }, + "content": { + "name": "Pornography", + "description": "This content contains imagery of full-frontal nudity or explicit sexual activity." + } + }, + "sexual": { + "settings": { + "name": "Sexually Suggestive", + "description": "Content that does not meet the level of \"pornography\", but is still sexual. Some common examples have been selfies and \"hornyposting\" with underwear on, or partially naked (naked but covered, eg with hands or from side perspective). Sheer/see-through nipples may end up in this category." + }, + "account": { + "name": "Sexually Suggestive", + "description": "This account contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress." + }, + "content": { + "name": "Sexually Suggestive", + "description": "This content contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress." + } + }, + "nudity": { + "settings": { + "name": "Nudity", + "description": "Nudity which is not sexual, or that is primarily \"artistic\" in nature. For example: breastfeeding; classic art paintings and sculptures; newspaper images with some nudity; fashion modeling. \"Erotic photography\" is likely to end up in sexual or porn." + }, + "account": { + "name": "Nudity", + "description": "This account contains imagery which portrays nudity in a non-sexual or artistic setting." + }, + "content": { + "name": "Nudity", + "description": "This content contains imagery which portrays nudity in a non-sexual or artistic setting." + } + }, + "nsfl": { + "settings": { + "name": "NSFL", + "description": "\"Not Suitable For Life.\" This includes graphic images like the infamous \"goatse\" (don't look it up)." + }, + "account": { + "name": "Graphic Imagery (NSFL)", + "description": "This account contains graphic images which are often referred to as \"Not Suitable For Life.\"" + }, + "content": { + "name": "Graphic Imagery (NSFL)", + "description": "This content contains graphic images which are often referred to as \"Not Suitable For Life.\"" + } + }, + "corpse": { + "settings": { + "name": "Corpse", + "description": "Visual image of a dead human body in any context. Includes war images, hanging, funeral caskets. Does not include all figurative cases (cartoons), but can include realistic figurative images or renderings." + }, + "account": { + "name": "Graphic Imagery (Corpse)", + "description": "This account contains images of a dead human body in any context. Includes war images, hanging, funeral caskets." + }, + "content": { + "name": "Graphic Imagery (Corpse)", + "description": "This content contains images of a dead human body in any context. Includes war images, hanging, funeral caskets." + } + }, + "gore": { + "settings": { + "name": "Gore", + "description": "Intended for shocking images, typically involving blood or visible wounds." + }, + "account": { + "name": "Graphic Imagery (Gore)", + "description": "This account contains shocking images involving blood or visible wounds." + }, + "content": { + "name": "Graphic Imagery (Gore)", + "description": "This content contains shocking images involving blood or visible wounds." + } + }, + "torture": { + "settings": { + "name": "Torture", + "description": "Depictions of torture of a human or animal (animal cruelty)." + }, + "account": { + "name": "Graphic Imagery (Torture)", + "description": "This account contains depictions of torture of a human or animal." + }, + "content": { + "name": "Graphic Imagery (Torture)", + "description": "This content contains depictions of torture of a human or animal." + } + }, + "self-harm": { + "settings": { + "name": "Self-Harm", + "description": "A visual depiction (photo or figurative) of cutting, suicide, or similar." + }, + "account": { + "name": "Graphic Imagery (Self-Harm)", + "description": "This account includes depictions of cutting, suicide, or other forms of self-harm." + }, + "content": { + "name": "Graphic Imagery (Self-Harm)", + "description": "This content includes depictions of cutting, suicide, or other forms of self-harm." + } + }, + "intolerant-race": { + "settings": { + "name": "Racial Intolerance", + "description": "Hateful or intolerant content related to race." + }, + "account": { + "name": "Intolerance (Racial)", + "description": "This account includes hateful or intolerant content related to race." + }, + "content": { + "name": "Intolerance (Racial)", + "description": "This content includes hateful or intolerant views related to race." + } + }, + "intolerant-gender": { + "settings": { + "name": "Gender Intolerance", + "description": "Hateful or intolerant content related to gender or gender identity." + }, + "account": { + "name": "Intolerance (Gender)", + "description": "This account includes hateful or intolerant content related to gender or gender identity." + }, + "content": { + "name": "Intolerance (Gender)", + "description": "This content includes hateful or intolerant views related to gender or gender identity." + } + }, + "intolerant-sexual-orientation": { + "settings": { + "name": "Sexual Orientation Intolerance", + "description": "Hateful or intolerant content related to sexual preferences." + }, + "account": { + "name": "Intolerance (Orientation)", + "description": "This account includes hateful or intolerant content related to sexual preferences." + }, + "content": { + "name": "Intolerance (Orientation)", + "description": "This content includes hateful or intolerant views related to sexual preferences." + } + }, + "intolerant-religion": { + "settings": { + "name": "Religious Intolerance", + "description": "Hateful or intolerant content related to religious views or practices." + }, + "account": { + "name": "Intolerance (Religious)", + "description": "This account includes hateful or intolerant content related to religious views or practices." + }, + "content": { + "name": "Intolerance (Religious)", + "description": "This content includes hateful or intolerant views related to religious views or practices." + } + }, + "intolerant": { + "settings": { + "name": "Intolerance", + "description": "A catchall for hateful or intolerant content which is not covered elsewhere." + }, + "account": { + "name": "Intolerance", + "description": "This account includes hateful or intolerant content." + }, + "content": { + "name": "Intolerance", + "description": "This content includes hateful or intolerant views." + } + }, + "icon-intolerant": { + "settings": { + "name": "Intolerant Iconography", + "description": "Visual imagery associated with a hate group, such as the KKK or Nazi, in any context (supportive, critical, documentary, etc)." + }, + "account": { + "name": "Intolerant Iconography", + "description": "This account includes imagery associated with a hate group such as the KKK or Nazis. This warning may apply to content any context, including critical or documentary purposes." + }, + "content": { + "name": "Intolerant Iconography", + "description": "This content includes imagery associated with a hate group such as the KKK or Nazis. This warning may apply to content any context, including critical or documentary purposes." + } + }, + "threat": { + "settings": { + "name": "Threats", + "description": "Statements or imagery published with the intent to threaten, intimidate, or harm." + }, + "account": { + "name": "Threats", + "description": "The moderators believe this account has published statements or imagery with the intent to threaten, intimidate, or harm others." + }, + "content": { + "name": "Threats", + "description": "The moderators believe this content was published with the intent to threaten, intimidate, or harm others." + } + }, + "spoiler": { + "settings": { + "name": "Spoiler", + "description": "Discussion about film, TV, etc which gives away plot points." + }, + "account": { + "name": "Spoiler Warning", + "description": "This account contains discussion about film, TV, etc which gives away plot points." + }, + "content": { + "name": "Spoiler Warning", + "description": "This content contains discussion about film, TV, etc which gives away plot points." + } + }, + "spam": { + "settings": { + "name": "Spam", + "description": "Repeat, low-quality messages which are clearly not designed to add to a conversation or space." + }, + "account": { + "name": "Spam", + "description": "This account publishes repeat, low-quality messages which are clearly not designed to add to a conversation or space." + }, + "content": { + "name": "Spam", + "description": "This content is a part of repeat, low-quality messages which are clearly not designed to add to a conversation or space." + } + }, + "account-security": { + "settings": { + "name": "Security Concerns", + "description": "Content designed to hijack user accounts such as a phishing attack." + }, + "account": { + "name": "Security Warning", + "description": "This account has published content designed to hijack user accounts such as a phishing attack." + }, + "content": { + "name": "Security Warning", + "description": "This content is designed to hijack user accounts such as a phishing attack." + } + }, + "net-abuse": { + "settings": { + "name": "Network Attacks", + "description": "Content designed to attack network systems such as denial-of-service attacks." + }, + "account": { + "name": "Network Attack Warning", + "description": "This account has published content designed to attack network systems such as denial-of-service attacks." + }, + "content": { + "name": "Network Attack Warning", + "description": "This content is designed to attack network systems such as denial-of-service attacks." + } + }, + "impersonation": { + "settings": { + "name": "Impersonation", + "description": "Accounts which falsely assert some identity." + }, + "account": { + "name": "Impersonation Warning", + "description": "The moderators believe this account is lying about their identity." + }, + "content": { + "name": "Impersonation Warning", + "description": "The moderators believe this account is lying about their identity." + } + }, + "scam": { + "settings": { + "name": "Scam", + "description": "Fraudulent content." + }, + "account": { + "name": "Scam Warning", + "description": "The moderators believe this account publishes fraudulent content." + }, + "content": { + "name": "Scam Warning", + "description": "The moderators believe this is fraudulent content." + } + } +} \ No newline at end of file diff --git a/packages/api/definitions/locale/en/proposed-label-groups.json b/packages/api/definitions/locale/en/proposed-label-groups.json new file mode 100644 index 00000000000..5658f0cedea --- /dev/null +++ b/packages/api/definitions/locale/en/proposed-label-groups.json @@ -0,0 +1,38 @@ +{ + "system": { + "name": "System", + "description": "Moderator overrides for special cases." + }, + "legal": { + "name": "Legal", + "description": "Content removed for legal reasons." + }, + "sexual": { + "name": "Adult Content", + "description": "Content which is sexual in nature." + }, + "violence": { + "name": "Violence", + "description": "Content which is violent or deeply disturbing." + }, + "intolerance": { + "name": "Intolerance", + "description": "Content or behavior which is hateful or intolerant toward a group of people." + }, + "rude": { + "name": "Rude", + "description": "Behavior which is rude toward other users." + }, + "curation": { + "name": "Curational", + "description": "Subjective moderation geared towards curating a more positive environment." + }, + "spam": { + "name": "Spam", + "description": "Content which doesn't add to the conversation." + }, + "misinfo": { + "name": "Misinformation", + "description": "Content which misleads or defrauds users." + } +} \ No newline at end of file diff --git a/packages/api/definitions/locale/en/proposed-labels.json b/packages/api/definitions/locale/en/proposed-labels.json new file mode 100644 index 00000000000..48702af87a2 --- /dev/null +++ b/packages/api/definitions/locale/en/proposed-labels.json @@ -0,0 +1,632 @@ +{ + "!hide": { + "settings": { + "name": "Moderator Hide", + "description": "Moderator has chosen to hide the content." + }, + "account": { + "name": "Content Blocked", + "description": "This account has been hidden by the moderators." + }, + "content": { + "name": "Content Blocked", + "description": "This content has been hidden by the moderators." + } + }, + "!no-promote": { + "settings": { + "name": "Moderator Filter", + "description": "Moderator has chosen to filter the content from feeds." + }, + "account": { + "name": "N/A", + "description": "N/A" + }, + "content": { + "name": "N/A", + "description": "N/A" + } + }, + "!warn": { + "settings": { + "name": "Moderator Warn", + "description": "Moderator has chosen to set a general warning on the content." + }, + "account": { + "name": "Content Warning", + "description": "This account has received a general warning from moderators." + }, + "content": { + "name": "Content Warning", + "description": "This content has received a general warning from moderators." + } + }, + "nudity-nonconsensual": { + "settings": { + "name": "Nonconsensual Nudity", + "description": "Nudity or sexual material which has been identified as being shared without the consent of the subjects." + }, + "account": { + "name": "Nonconsensual Nudity", + "description": "This account has triggered the Nonconsensual Nudity Review systems. This may be in error, so please do not jump to conclusions while the account is under review. This warning will be lifted if the review was triggered incorrectly. Otherwise, the account will be removed from the network." + }, + "content": { + "name": "Nonconsensual Nudity", + "description": "This content has triggered the Nonconsensual Nudity Review systems. This may be in error, so please do not jump to conclusions while the account is under review. This warning will be lifted if the review was triggered incorrectly. Otherwise, the account will be removed from the network." + } + }, + "dmca-violation": { + "settings": { + "name": "Copyright Violation", + "description": "The content has received a DMCA takedown request." + }, + "account": { + "name": "Copyright Violation", + "description": "This account has received a DMCA takedown request. It will be restored if the concerns can be resolved." + }, + "content": { + "name": "Copyright Violation", + "description": "This content has received a DMCA takedown request. It will be restored if the concerns can be resolved." + } + }, + "doxxing": { + "settings": { + "name": "Doxxing", + "description": "Information that reveals private information about someone which has been shared without the consent of the subject." + }, + "account": { + "name": "Doxxing", + "description": "This account has been reported to publish private information about someone without their consent. This report is currently under review." + }, + "content": { + "name": "Doxxing", + "description": "This content has been reported to include private information about someone without their consent." + } + }, + "porn": { + "settings": { + "name": "Pornography", + "description": "Images of full-frontal nudity (genitalia) in any sexualized context, or explicit sexual activity (meaning contact with genitalia or breasts) even if partially covered. Includes graphic sexual cartoons (often jokes/memes)." + }, + "account": { + "name": "Pornography", + "description": "This account contains imagery of full-frontal nudity or explicit sexual activity." + }, + "content": { + "name": "Pornography", + "description": "This content contains imagery of full-frontal nudity or explicit sexual activity." + } + }, + "sexual": { + "settings": { + "name": "Sexually Suggestive", + "description": "Content that does not meet the level of \"pornography\", but is still sexual. Some common examples have been selfies and \"hornyposting\" with underwear on, or partially naked (naked but covered, eg with hands or from side perspective). Sheer/see-through nipples may end up in this category." + }, + "account": { + "name": "Sexually Suggestive", + "description": "This account contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress." + }, + "content": { + "name": "Sexually Suggestive", + "description": "This content contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress." + } + }, + "nudity": { + "settings": { + "name": "Nudity", + "description": "Nudity which is not sexual, or that is primarily \"artistic\" in nature. For example: breastfeeding; classic art paintings and sculptures; newspaper images with some nudity; fashion modeling. \"Erotic photography\" is likely to end up in sexual or porn." + }, + "account": { + "name": "Nudity", + "description": "This account contains imagery which portrays nudity in a non-sexual or artistic setting." + }, + "content": { + "name": "Nudity", + "description": "This content contains imagery which portrays nudity in a non-sexual or artistic setting." + } + }, + "nsfl": { + "settings": { + "name": "NSFL", + "description": "\"Not Suitable For Life.\" This includes graphic images like the infamous \"goatse\" (don't look it up)." + }, + "account": { + "name": "Graphic Imagery (NSFL)", + "description": "This account contains graphic images which are often referred to as \"Not Suitable For Life.\"" + }, + "content": { + "name": "Graphic Imagery (NSFL)", + "description": "This content contains graphic images which are often referred to as \"Not Suitable For Life.\"" + } + }, + "corpse": { + "settings": { + "name": "Corpse", + "description": "Visual image of a dead human body in any context. Includes war images, hanging, funeral caskets. Does not include all figurative cases (cartoons), but can include realistic figurative images or renderings." + }, + "account": { + "name": "Graphic Imagery (Corpse)", + "description": "This account contains images of a dead human body in any context. Includes war images, hanging, funeral caskets." + }, + "content": { + "name": "Graphic Imagery (Corpse)", + "description": "This content contains images of a dead human body in any context. Includes war images, hanging, funeral caskets." + } + }, + "gore": { + "settings": { + "name": "Gore", + "description": "Intended for shocking images, typically involving blood or visible wounds." + }, + "account": { + "name": "Graphic Imagery (Gore)", + "description": "This account contains shocking images involving blood or visible wounds." + }, + "content": { + "name": "Graphic Imagery (Gore)", + "description": "This content contains shocking images involving blood or visible wounds." + } + }, + "torture": { + "settings": { + "name": "Torture", + "description": "Depictions of torture of a human or animal (animal cruelty)." + }, + "account": { + "name": "Graphic Imagery (Torture)", + "description": "This account contains depictions of torture of a human or animal." + }, + "content": { + "name": "Graphic Imagery (Torture)", + "description": "This content contains depictions of torture of a human or animal." + } + }, + "self-harm": { + "settings": { + "name": "Self-Harm", + "description": "A visual depiction (photo or figurative) of cutting, suicide, or similar." + }, + "account": { + "name": "Graphic Imagery (Self-Harm)", + "description": "This account includes depictions of cutting, suicide, or other forms of self-harm." + }, + "content": { + "name": "Graphic Imagery (Self-Harm)", + "description": "This content includes depictions of cutting, suicide, or other forms of self-harm." + } + }, + "intolerant-race": { + "settings": { + "name": "Racial Intolerance", + "description": "Hateful or intolerant content related to race." + }, + "account": { + "name": "Intolerance (Racial)", + "description": "This account includes hateful or intolerant content related to race." + }, + "content": { + "name": "Intolerance (Racial)", + "description": "This content includes hateful or intolerant views related to race." + } + }, + "intolerant-gender": { + "settings": { + "name": "Gender Intolerance", + "description": "Hateful or intolerant content related to gender or gender identity." + }, + "account": { + "name": "Intolerance (Gender)", + "description": "This account includes hateful or intolerant content related to gender or gender identity." + }, + "content": { + "name": "Intolerance (Gender)", + "description": "This content includes hateful or intolerant views related to gender or gender identity." + } + }, + "intolerant-sexual-orientation": { + "settings": { + "name": "Sexual Orientation Intolerance", + "description": "Hateful or intolerant content related to sexual preferences." + }, + "account": { + "name": "Intolerance (Orientation)", + "description": "This account includes hateful or intolerant content related to sexual preferences." + }, + "content": { + "name": "Intolerance (Orientation)", + "description": "This content includes hateful or intolerant views related to sexual preferences." + } + }, + "intolerant-religion": { + "settings": { + "name": "Religious Intolerance", + "description": "Hateful or intolerant content related to religious views or practices." + }, + "account": { + "name": "Intolerance (Religious)", + "description": "This account includes hateful or intolerant content related to religious views or practices." + }, + "content": { + "name": "Intolerance (Religious)", + "description": "This content includes hateful or intolerant views related to religious views or practices." + } + }, + "intolerant": { + "settings": { + "name": "Intolerance", + "description": "A catchall for hateful or intolerant content which is not covered elsewhere." + }, + "account": { + "name": "Intolerance", + "description": "This account includes hateful or intolerant content." + }, + "content": { + "name": "Intolerance", + "description": "This content includes hateful or intolerant views." + } + }, + "icon-intolerant": { + "settings": { + "name": "Intolerant Iconography", + "description": "Visual imagery associated with a hate group, such as the KKK or Nazi, in any context (supportive, critical, documentary, etc)." + }, + "account": { + "name": "Intolerant Iconography", + "description": "This account includes imagery associated with a hate group such as the KKK or Nazis. This warning may apply to content any context, including critical or documentary purposes." + }, + "content": { + "name": "Intolerant Iconography", + "description": "This content includes imagery associated with a hate group such as the KKK or Nazis. This warning may apply to content any context, including critical or documentary purposes." + } + }, + "trolling": { + "settings": { + "name": "Trolling", + "description": "Content which is intended to produce a negative reaction from other users." + }, + "account": { + "name": "Trolling", + "description": "The moderators believe this account has published content intended to inflame users." + }, + "content": { + "name": "Trolling", + "description": "The moderators believe this content is intended to inflame users." + } + }, + "harassment": { + "settings": { + "name": "Harassment", + "description": "Repeated posts directed at a user or a group of users with the intent to produce a negative reaction." + }, + "account": { + "name": "Harassment", + "description": "The moderators believe this account has published content directed at a user or a group of users with the intent to inflame." + }, + "content": { + "name": "Harassment", + "description": "The moderators believe this content is directed at a user or a group of users with the intent to inflame." + } + }, + "bullying": { + "settings": { + "name": "Bullying", + "description": "Statements or imagery published with the intent to bully, humiliate, or degrade." + }, + "account": { + "name": "Bullying", + "description": "The moderators believe this account has published statements or imagery published with the intent to bully, humiliate, or degrade others." + }, + "content": { + "name": "Bullying", + "description": "The moderators believe this content was published with the intent to bully, humiliate, or degrade others." + } + }, + "threat": { + "settings": { + "name": "Threats", + "description": "Statements or imagery published with the intent to threaten, intimidate, or harm." + }, + "account": { + "name": "Threats", + "description": "The moderators believe this account has published statements or imagery with the intent to threaten, intimidate, or harm others." + }, + "content": { + "name": "Threats", + "description": "The moderators believe this content was published with the intent to threaten, intimidate, or harm others." + } + }, + "disgusting": { + "settings": { + "name": "Disgusting", + "description": "Content which is gross, like an image of poop." + }, + "account": { + "name": "Warning: Disgusting", + "description": "The moderators believe this account contains content which users may find disgusting." + }, + "content": { + "name": "Warning: Disgusting", + "description": "The moderators believe users may find this content disgusting." + } + }, + "upsetting": { + "settings": { + "name": "Upsetting", + "description": "Content which is upsetting, like a video of an accident." + }, + "account": { + "name": "Warning: Upsetting", + "description": "The moderators believe this account contains content which users may find upsetting." + }, + "content": { + "name": "Warning: Upsetting", + "description": "The moderators believe users may find this content upsetting." + } + }, + "profane": { + "settings": { + "name": "Profane", + "description": "Content which includes excessive swearing or violates common sensibilities." + }, + "account": { + "name": "Warning: Profane", + "description": "The moderators believe this account contains content which users may find profane." + }, + "content": { + "name": "Warning: Profane", + "description": "The moderators believe users may find this content profane." + } + }, + "politics": { + "settings": { + "name": "Politics", + "description": "Anything that discusses politics or political discourse." + }, + "account": { + "name": "Warning: Politics", + "description": "This is not a violation. The moderators believe this account discusses politics or political discourse. This warning is only provided for users who wish to reduce the amount of politics in their experience." + }, + "content": { + "name": "Warning: Politics", + "description": "This is not a violation. The moderators believe this content discusses politics or political discourse. This warning is only provided for users who wish to reduce the amount of politics in their experience." + } + }, + "troubling": { + "settings": { + "name": "Troubling", + "description": "Content which can be difficult to process such as bad news." + }, + "account": { + "name": "Warning: Troubling", + "description": "This is not a violation. The moderators believe this account discusses topics which can be difficult to process. This warning is only provided for users who wish to reduce the amount of troubling discussion in their experience." + }, + "content": { + "name": "Warning: Troubling", + "description": "This is not a violation. The moderators believe this content discusses topics which can be difficult to process. This warning is only provided for users who wish to reduce the amount of troubling discussion in their experience." + } + }, + "negative": { + "settings": { + "name": "Negative", + "description": "Statements which are critical, pessimistic, or generally negative." + }, + "account": { + "name": "Warning: Negative", + "description": "This is not a violation. The moderators believe this account publishes statements which are critical, pessimistic, or generally negative. This warning is only provided for users who wish to reduce the amount of negativity in their experience." + }, + "content": { + "name": "Warning: Negative", + "description": "This is not a violation. The moderators believe this content is critical, pessimistic, or generally negative. This warning is only provided for users who wish to reduce the amount of negativity in their experience." + } + }, + "discourse": { + "settings": { + "name": "Discourse", + "description": "Drama, typically about some topic which is currently active in the network." + }, + "account": { + "name": "Warning: Discourse", + "description": "This is not a violation. The moderators believe this account publishes statements regarding in-network drama or disputes (aka \"discourse\"). This warning is only provided for users who wish to reduce the amount of negativity in their experience." + }, + "content": { + "name": "Warning: Discourse", + "description": "This is not a violation. The moderators believe this content relates to in-network drama or disputes (aka \"discourse\"). This warning is only provided for users who wish to reduce the amount of negativity in their experience." + } + }, + "spoiler": { + "settings": { + "name": "Spoiler", + "description": "Discussion about film, TV, etc which gives away plot points." + }, + "account": { + "name": "Spoiler Warning", + "description": "This account contains discussion about film, TV, etc which gives away plot points." + }, + "content": { + "name": "Spoiler Warning", + "description": "This content contains discussion about film, TV, etc which gives away plot points." + } + }, + "spam": { + "settings": { + "name": "Spam", + "description": "Repeat, low-quality messages which are clearly not designed to add to a conversation or space." + }, + "account": { + "name": "Spam", + "description": "This account publishes repeat, low-quality messages which are clearly not designed to add to a conversation or space." + }, + "content": { + "name": "Spam", + "description": "This content is a part of repeat, low-quality messages which are clearly not designed to add to a conversation or space." + } + }, + "clickbait": { + "settings": { + "name": "Clickbait", + "description": "Low-quality content that's designed to get users to open an external link by appearing more engaging than it is." + }, + "account": { + "name": "Clickbait", + "description": "The moderators believe this account publishes low-quality content that's designed to get users to open an external link by appearing more engaging than it is." + }, + "content": { + "name": "Clickbait", + "description": "The moderators believe this is low-quality content that's designed to get users to open an external link by appearing more engaging than it is." + } + }, + "shill": { + "settings": { + "name": "Shilling", + "description": "Over-enthusiastic promotion of a technology, product, or service, especially when there is a financial conflict of interest." + }, + "account": { + "name": "Shill", + "description": "The moderators believe this account participates in over-enthusiastic promotion of a technology, product, or service." + }, + "content": { + "name": "Shilling", + "description": "The moderators believe this content is in over-enthusiastic promotion of a technology, product, or service." + } + }, + "promotion": { + "settings": { + "name": "Promotion", + "description": "Advertising or blunt marketing of a commercial service or product." + }, + "account": { + "name": "Promotion", + "description": "The moderators believe this account engages in advertising or blunt marketing of a commercial service or product." + }, + "content": { + "name": "Promotion", + "description": "The moderators believe this content is advertising or blunt marketing of a commercial service or product." + } + }, + "account-security": { + "settings": { + "name": "Security Concerns", + "description": "Content designed to hijack user accounts such as a phishing attack." + }, + "account": { + "name": "Security Warning", + "description": "This account has published content designed to hijack user accounts such as a phishing attack." + }, + "content": { + "name": "Security Warning", + "description": "This content is designed to hijack user accounts such as a phishing attack." + } + }, + "net-abuse": { + "settings": { + "name": "Network Attacks", + "description": "Content designed to attack network systems such as denial-of-service attacks." + }, + "account": { + "name": "Network Attack Warning", + "description": "This account has published content designed to attack network systems such as denial-of-service attacks." + }, + "content": { + "name": "Network Attack Warning", + "description": "This content is designed to attack network systems such as denial-of-service attacks." + } + }, + "impersonation": { + "settings": { + "name": "Impersonation", + "description": "Accounts which falsely assert some identity." + }, + "account": { + "name": "Impersonation Warning", + "description": "The moderators believe this account is lying about their identity." + }, + "content": { + "name": "Impersonation Warning", + "description": "The moderators believe this account is lying about their identity." + } + }, + "scam": { + "settings": { + "name": "Scam", + "description": "Fraudulent content." + }, + "account": { + "name": "Scam Warning", + "description": "The moderators believe this account publishes fraudulent content." + }, + "content": { + "name": "Scam Warning", + "description": "The moderators believe this is fraudulent content." + } + }, + "misinformation": { + "settings": { + "name": "Misinformation", + "description": "Lies with the intent to deceive." + }, + "account": { + "name": "Misinformation Warning", + "description": "The moderators believe this account has published lies with the intent to deceive." + }, + "content": { + "name": "Misinformation Warning", + "description": "The moderators believe this content contains lies with the intent to deceive." + } + }, + "unverified": { + "settings": { + "name": "Unverified Claims", + "description": "Assertions which have not been verified by a trusted source." + }, + "account": { + "name": "Unverified Claims Warning", + "description": "The moderators believe this account has published claims which have not been verified by a trusted source." + }, + "content": { + "name": "Unverified Claims Warning", + "description": "The moderators believe this content contains claims which have not been verified by a trusted source." + } + }, + "manipulated": { + "settings": { + "name": "Manipulated Media", + "description": "Content which misrepresents a person or event by modifying the source material." + }, + "account": { + "name": "Manipulated Media Warning", + "description": "The moderators believe this account has published content which misrepresents a person or event by modifying the source material." + }, + "content": { + "name": "Manipulated Media Warning", + "description": "The moderators believe this content contains misrepresentations of a person or event by modifying the source material." + } + }, + "fringe": { + "settings": { + "name": "Conspiracy Theories", + "description": "Fringe views which lack evidence." + }, + "account": { + "name": "Conspiracy Theories Warning", + "description": "The moderators believe this account has published fringe views which lack evidence." + }, + "content": { + "name": "Conspiracy Theories Warning", + "description": "The moderators believe this content contains fringe views which lack evidence." + } + }, + "bullshit": { + "settings": { + "name": "Bullshit", + "description": "Content which is not technically wrong or lying, but misleading through omission or re-contextualization." + }, + "account": { + "name": "Bullshit Warning", + "description": "The moderators believe this account has published content which is not technically wrong or lying, but misleading through omission or re-contextualization." + }, + "content": { + "name": "Bullshit Warning", + "description": "The moderators believe this content includes statements which are not technically wrong or lying, but are misleading through omission or re-contextualization." + } + } +} \ No newline at end of file diff --git a/packages/api/definitions/moderation-behaviors.d.ts b/packages/api/definitions/moderation-behaviors.d.ts new file mode 100644 index 00000000000..5f6e2df81ca --- /dev/null +++ b/packages/api/definitions/moderation-behaviors.d.ts @@ -0,0 +1,48 @@ +import type { LabelPreference } from '../src' + +export interface ModerationBehaviorResult { + cause?: string + filter?: boolean + blur?: boolean + alert?: boolean + noOverride?: boolean +} + +export interface ModerationBehaviorScenario { + cfg: string + subject: 'post' | 'profile' | 'userlist' | 'feedgen' + author: string + quoteAuthor?: string + labels: { + post?: string[] + profile?: string[] + account?: string[] + quotedPost?: string[] + quotedAccount?: string[] + } + behaviors: { + content?: ModerationBehaviorResult + avatar?: ModerationBehaviorResult + embed?: ModerationBehaviorResult + } +} + +export interface ModerationBehaviors { + users: Record< + string, + { + blocking: boolean + blockedBy: boolean + muted: boolean + mutedByList: boolean + } + > + configurations: Record< + string, + { + adultContentEnabled: boolean + settings: Record + } + > + scenarios: Record +} diff --git a/packages/api/definitions/post-moderation-behaviors.json b/packages/api/definitions/post-moderation-behaviors.json new file mode 100644 index 00000000000..9b381d96c8e --- /dev/null +++ b/packages/api/definitions/post-moderation-behaviors.json @@ -0,0 +1,879 @@ + +{ + "users": { + "self": {"blocking": false, "blockedBy": false, "muted": false, "mutedByList": false}, + "alice": {"blocking": false, "blockedBy": false, "muted": false, "mutedByList": false}, + "bob": {"blocking": true, "blockedBy": false, "muted": false, "mutedByList": false}, + "carla": {"blocking": false, "blockedBy": true, "muted": false, "mutedByList": false}, + "dan": {"blocking": false, "blockedBy": false, "muted": true, "mutedByList": false}, + "elise": {"blocking": false, "blockedBy": false, "muted": false, "mutedByList": true}, + "fern": {"blocking": true, "blockedBy": true, "muted": false, "mutedByList": false} + }, + "configurations": { + "none": {}, + "adult-disabled": { + "adultContentEnabled": false + }, + "intolerant-hide": { + "settings": {"intolerant": "hide"} + }, + "intolerant-warn": { + "settings": {"intolerant": "warn"} + }, + "intolerant-ignore": { + "settings": {"intolerant": "ignore"} + }, + "porn-hide": { + "adultContentEnabled": true, + "settings": {"porn": "hide"} + }, + "porn-warn": { + "adultContentEnabled": true, + "settings": {"porn": "warn"} + }, + "porn-ignore": { + "adultContentEnabled": true, + "settings": {"porn": "ignore"} + }, + "scam-hide": { + "settings": {"scam": "hide"} + }, + "scam-warn": { + "settings": {"scam": "warn"} + }, + "scam-ignore": { + "settings": {"scam": "ignore"} + }, + "porn-hide-intolerant-hide": { + "adultContentEnabled": true, + "settings": {"porn": "hide", "intolerant": "hide"} + }, + "porn-hide-intolerant-warn": { + "adultContentEnabled": true, + "settings": {"porn": "hide", "intolerant": "warn"} + }, + "porn-warn-intolerant-hide": { + "adultContentEnabled": true, + "settings": {"porn": "warn", "intolerant": "hide"} + } + }, + "scenarios": { + "Imperative label ('!hide') on post": { + "cfg": "none", + "subject": "post", + "author": "alice", + "labels": {"post": ["!hide"]}, + "behaviors": { + "content": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true} + } + }, + "Imperative label ('!hide') on author profile": { + "cfg": "none", + "subject": "post", + "author": "alice", + "labels": {"profile": ["!hide"]}, + "behaviors": { + "avatar": {"cause": "label:!hide", "blur": true, "noOverride": true} + } + }, + "Imperative label ('!hide') on author account": { + "cfg": "none", + "subject": "post", + "author": "alice", + "labels": {"account": ["!hide"]}, + "behaviors": { + "content": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true}, + "avatar": {"cause": "label:!hide", "blur": true, "noOverride": true} + } + }, + "Imperative label ('!hide') on quoted post": { + "cfg": "none", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedPost": ["!hide"]}, + "behaviors": { + "content": {"cause": "label:!hide", "filter": true}, + "embed": {"cause": "label:!hide", "blur": true, "noOverride": true} + } + }, + "Imperative label ('!hide') on quoted author account": { + "cfg": "none", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedAccount": ["!hide"]}, + "behaviors": { + "content": {"cause": "label:!hide", "filter": true}, + "embed": {"cause": "label:!hide", "blur": true, "noOverride": true} + } + }, + + "Imperative label ('!no-promote') on post": { + "cfg": "none", + "subject": "post", + "author": "alice", + "labels": {"post": ["!no-promote"]}, + "behaviors": { + "content": {"cause": "label:!no-promote", "filter": true} + } + }, + "Imperative label ('!no-promote') on author profile": { + "cfg": "none", + "subject": "post", + "author": "alice", + "labels": {"profile": ["!no-promote"]}, + "behaviors": {} + }, + "Imperative label ('!no-promote') on author account": { + "cfg": "none", + "subject": "post", + "author": "alice", + "labels": {"account": ["!no-promote"]}, + "behaviors": { + "content": {"cause": "label:!no-promote", "filter": true} + } + }, + "Imperative label ('!no-promote') on quoted post": { + "cfg": "none", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedPost": ["!no-promote"]}, + "behaviors": { + "content": {"cause": "label:!no-promote", "filter": true} + } + }, + "Imperative label ('!no-promote') on quoted author account": { + "cfg": "none", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedAccount": ["!no-promote"]}, + "behaviors": { + "content": {"cause": "label:!no-promote", "filter": true} + } + }, + + "Imperative label ('!warn') on post": { + "cfg": "none", + "subject": "post", + "author": "alice", + "labels": {"post": ["!warn"]}, + "behaviors": { + "content": {"cause": "label:!warn", "blur": true} + } + }, + "Imperative label ('!warn') on author profile": { + "cfg": "none", + "subject": "post", + "author": "alice", + "labels": {"profile": ["!warn"]}, + "behaviors": { + "avatar": {"blur": true} + } + }, + "Imperative label ('!warn') on author account": { + "cfg": "none", + "subject": "post", + "author": "alice", + "labels": {"account": ["!warn"]}, + "behaviors": { + "content": {"cause": "label:!warn", "blur": true}, + "avatar": {"blur": true} + } + }, + "Imperative label ('!warn') on quoted post": { + "cfg": "none", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedPost": ["!warn"]}, + "behaviors": { + "embed": {"cause": "label:!warn", "blur": true} + } + }, + "Imperative label ('!warn') on quoted author account": { + "cfg": "none", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedAccount": ["!warn"]}, + "behaviors": { + "embed": {"cause": "label:!warn", "blur": true} + } + }, + + "Blur label ('intolerant') on post (hide)": { + "cfg": "intolerant-hide", + "subject": "post", + "author": "alice", + "labels": {"post": ["intolerant"]}, + "behaviors": { + "content": {"cause": "label:intolerant", "filter": true, "blur": true} + } + }, + "Blur label ('intolerant') on author profile (hide)": { + "cfg": "intolerant-hide", + "subject": "post", + "author": "alice", + "labels": {"profile": ["intolerant"]}, + "behaviors": { + "avatar": {"blur": true} + } + }, + "Blur label ('intolerant') on author account (hide)": { + "cfg": "intolerant-hide", + "subject": "post", + "author": "alice", + "labels": {"account": ["intolerant"]}, + "behaviors": { + "content": {"cause": "label:intolerant", "filter": true, "blur": true}, + "avatar": {"blur": true} + } + }, + "Blur label ('intolerant') on quoted post (hide)": { + "cfg": "intolerant-hide", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedPost": ["intolerant"]}, + "behaviors": { + "content": {"cause": "label:intolerant", "filter": true}, + "embed": {"cause": "label:intolerant", "blur": true} + } + }, + "Blur label ('intolerant') on quoted author account (hide)": { + "cfg": "intolerant-hide", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedAccount": ["intolerant"]}, + "behaviors": { + "content": {"cause": "label:intolerant", "filter": true}, + "embed": {"cause": "label:intolerant", "blur": true} + } + }, + + "Blur label ('intolerant') on post (warn)": { + "cfg": "intolerant-warn", + "subject": "post", + "author": "alice", + "labels": {"post": ["intolerant"]}, + "behaviors": { + "content": {"cause": "label:intolerant", "blur": true} + } + }, + "Blur label ('intolerant') on author profile (warn)": { + "cfg": "intolerant-warn", + "subject": "post", + "author": "alice", + "labels": {"profile": ["intolerant"]}, + "behaviors": { + "avatar": {"blur": true} + } + }, + "Blur label ('intolerant') on author account (warn)": { + "cfg": "intolerant-warn", + "subject": "post", + "author": "alice", + "labels": {"account": ["intolerant"]}, + "behaviors": { + "content": {"cause": "label:intolerant", "blur": true}, + "avatar": {"blur": true} + } + }, + "Blur label ('intolerant') on quoted post (warn)": { + "cfg": "intolerant-warn", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedPost": ["intolerant"]}, + "behaviors": { + "embed": {"cause": "label:intolerant", "blur": true} + } + }, + "Blur label ('intolerant') on quoted author account (warn)": { + "cfg": "intolerant-warn", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedPost": ["intolerant"]}, + "behaviors": { + "embed": {"cause": "label:intolerant", "blur": true} + } + }, + + "Blur label ('intolerant') on post (ignore)": { + "cfg": "intolerant-ignore", + "subject": "post", + "author": "alice", + "labels": {"post": ["intolerant"]}, + "behaviors": {} + }, + "Blur label ('intolerant') on author profile (ignore)": { + "cfg": "intolerant-ignore", + "subject": "post", + "author": "alice", + "labels": {"profile": ["intolerant"]}, + "behaviors": {} + }, + "Blur label ('intolerant') on author account (ignore)": { + "cfg": "intolerant-ignore", + "subject": "post", + "author": "alice", + "labels": {"account": ["intolerant"]}, + "behaviors": {} + }, + "Blur label ('intolerant') on quoted post (ignore)": { + "cfg": "intolerant-ignore", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedPost": ["intolerant"]}, + "behaviors": {} + }, + "Blur label ('intolerant') on quoted author account (ignore)": { + "cfg": "intolerant-ignore", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedAccount": ["intolerant"]}, + "behaviors": {} + }, + + "Blur-media label ('porn') on post (hide)": { + "cfg": "porn-hide", + "subject": "post", + "author": "alice", + "labels": {"post": ["porn"]}, + "behaviors": { + "content": {"cause": "label:porn", "filter": true}, + "embed": {"cause": "label:porn", "blur": true} + } + }, + "Blur-media label ('porn') on author profile (hide)": { + "cfg": "porn-hide", + "subject": "post", + "author": "alice", + "labels": {"profile": ["porn"]}, + "behaviors": { + "avatar": {"blur": true} + } + }, + "Blur-media label ('porn') on author account (hide)": { + "cfg": "porn-hide", + "subject": "post", + "author": "alice", + "labels": {"account": ["porn"]}, + "behaviors": { + "content": {"cause": "label:porn", "filter": true}, + "avatar": {"blur": true}, + "embed": {"cause": "label:porn", "blur": true} + } + }, + "Blur-media label ('porn') on quoted post (hide)": { + "cfg": "porn-hide", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedPost": ["porn"]}, + "behaviors": { + "content": {"cause": "label:porn", "filter": true} + } + }, + "Blur-media label ('porn') on quoted author account (hide)": { + "cfg": "porn-hide", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedAccount": ["porn"]}, + "behaviors": { + "content": {"cause": "label:porn", "filter": true} + } + }, + + "Blur-media label ('porn') on post (warn)": { + "cfg": "porn-warn", + "subject": "post", + "author": "alice", + "labels": {"post": ["porn"]}, + "behaviors": { + "embed": {"cause": "label:porn", "blur": true} + } + }, + "Blur-media label ('porn') on author profile (warn)": { + "cfg": "porn-warn", + "subject": "post", + "author": "alice", + "labels": {"profile": ["porn"]}, + "behaviors": { + "avatar": {"blur": true} + } + }, + "Blur-media label ('porn') on author account (warn)": { + "cfg": "porn-warn", + "subject": "post", + "author": "alice", + "labels": {"account": ["porn"]}, + "behaviors": { + "avatar": {"blur": true}, + "embed": {"cause": "label:porn", "blur": true} + } + }, + "Blur-media label ('porn') on quoted post (warn)": { + "cfg": "porn-warn", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedPost": ["porn"]}, + "behaviors": { + } + }, + "Blur-media label ('porn') on quoted author account (warn)": { + "cfg": "porn-warn", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedAccount": ["porn"]}, + "behaviors": { + } + }, + + "Blur-media label ('porn') on post (ignore)": { + "cfg": "porn-ignore", + "subject": "post", + "author": "alice", + "labels": {"post": ["porn"]}, + "behaviors": {} + }, + "Blur-media label ('porn') on author profile (ignore)": { + "cfg": "porn-ignore", + "subject": "post", + "author": "alice", + "labels": {"profile": ["porn"]}, + "behaviors": {} + }, + "Blur-media label ('porn') on author account (ignore)": { + "cfg": "porn-ignore", + "subject": "post", + "author": "alice", + "labels": {"account": ["porn"]}, + "behaviors": {} + }, + "Blur-media label ('porn') on quoted post (ignore)": { + "cfg": "porn-ignore", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedPost": ["porn"]}, + "behaviors": {} + }, + "Blur-media label ('porn') on quoted author account (ignore)": { + "cfg": "porn-ignore", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedAccount": ["porn"]}, + "behaviors": {} + }, + + "Notice label ('scam') on post (hide)": { + "cfg": "scam-hide", + "subject": "post", + "author": "alice", + "labels": {"post": ["scam"]}, + "behaviors": { + "content": {"cause": "label:scam", "filter": true, "alert": true} + } + }, + "Notice label ('scam') on author profile (hide)": { + "cfg": "scam-hide", + "subject": "post", + "author": "alice", + "labels": {"profile": ["scam"]}, + "behaviors": { + "avatar": {"alert": true} + } + }, + "Notice label ('scam') on author account (hide)": { + "cfg": "scam-hide", + "subject": "post", + "author": "alice", + "labels": {"account": ["scam"]}, + "behaviors": { + "content": {"cause": "label:scam", "filter": true, "alert": true}, + "avatar": {"alert": true} + } + }, + "Notice label ('scam') on quoted post (hide)": { + "cfg": "scam-hide", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedPost": ["scam"]}, + "behaviors": { + "content": {"cause": "label:scam", "filter": true}, + "embed": {"cause": "label:scam", "alert": true} + } + }, + "Notice label ('scam') on quoted author account (hide)": { + "cfg": "scam-hide", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedAccount": ["scam"]}, + "behaviors": { + "content": {"cause": "label:scam", "filter": true}, + "embed": {"cause": "label:scam", "alert": true} + } + }, + + "Notice label ('scam') on post (warn)": { + "cfg": "scam-warn", + "subject": "post", + "author": "alice", + "labels": {"post": ["scam"]}, + "behaviors": { + "content": {"cause": "label:scam", "alert": true} + } + }, + "Notice label ('scam') on author profile (warn)": { + "cfg": "scam-warn", + "subject": "post", + "author": "alice", + "labels": {"profile": ["scam"]}, + "behaviors": { + "avatar": {"alert": true} + } + }, + "Notice label ('scam') on author account (warn)": { + "cfg": "scam-warn", + "subject": "post", + "author": "alice", + "labels": {"account": ["scam"]}, + "behaviors": { + "content": {"cause": "label:scam", "alert": true}, + "avatar": {"alert": true} + } + }, + "Notice label ('scam') on quoted post (warn)": { + "cfg": "scam-warn", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedPost": ["scam"]}, + "behaviors": { + "embed": {"cause": "label:scam", "alert": true} + } + }, + "Notice label ('scam') on quoted author account (warn)": { + "cfg": "scam-warn", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedAccount": ["scam"]}, + "behaviors": { + "embed": {"cause": "label:scam", "alert": true} + } + }, + + "Notice label ('scam') on post (ignore)": { + "cfg": "scam-ignore", + "subject": "post", + "author": "alice", + "labels": {"post": ["scam"]}, + "behaviors": {} + }, + "Notice label ('scam') on author profile (ignore)": { + "cfg": "scam-ignore", + "subject": "post", + "author": "alice", + "labels": {"profile": ["scam"]}, + "behaviors": {} + }, + "Notice label ('scam') on author account (ignore)": { + "cfg": "scam-ignore", + "subject": "post", + "author": "alice", + "labels": {"account": ["scam"]}, + "behaviors": {} + }, + "Notice label ('scam') on quoted post (ignore)": { + "cfg": "scam-ignore", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedPost": ["scam"]}, + "behaviors": {} + }, + "Notice label ('scam') on quoted author account (ignore)": { + "cfg": "scam-ignore", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedAccount": ["scam"]}, + "behaviors": {} + }, + + "Adult-only label on post when adult content is disabled": { + "cfg": "adult-disabled", + "subject": "post", + "author": "alice", + "labels": {"post": ["porn"]}, + "behaviors": { + "content": {"cause": "label:porn", "filter": true, "noOverride": true}, + "embed": {"cause": "label:porn", "blur": true, "noOverride": true} + } + }, + "Adult-only label on author profile when adult content is disabled": { + "cfg": "adult-disabled", + "subject": "post", + "author": "alice", + "labels": {"profile": ["porn"]}, + "behaviors": { + "avatar": {"cause": "label:porn", "blur": true, "noOverride": true} + } + }, + "Adult-only label on author account when adult content is disabled": { + "cfg": "adult-disabled", + "subject": "post", + "author": "alice", + "labels": {"account": ["porn"]}, + "behaviors": { + "content": {"cause": "label:porn", "filter": true, "noOverride": true}, + "avatar": {"cause": "label:porn", "blur": true, "noOverride": true}, + "embed": {"cause": "label:porn", "blur": true, "noOverride": true} + } + }, + "Adult-only label on quoted post when adult content is disabled": { + "cfg": "adult-disabled", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedPost": ["porn"]}, + "behaviors": { + "content": {"cause": "label:porn", "filter": true} + } + }, + "Adult-only label on quoted author account when adult content is disabled": { + "cfg": "adult-disabled", + "subject": "post", + "author": "alice", + "quoteAuthor": "alice", + "labels": {"quotedAccount": ["porn"]}, + "behaviors": { + "content": {"cause": "label:porn", "filter": true} + } + }, + + "Self-post: Imperative label ('!hide') on post": { + "cfg": "none", + "subject": "post", + "author": "self", + "labels": {"post": ["!hide"]}, + "behaviors": { + "content": {"cause": "label:!hide", "alert": true} + } + }, + "Self-post: Imperative label ('!hide') on author profile": { + "cfg": "none", + "subject": "post", + "author": "self", + "labels": {"profile": ["!hide"]}, + "behaviors": {} + }, + "Self-post: Imperative label ('!hide') on author account": { + "cfg": "none", + "subject": "post", + "author": "self", + "labels": {"account": ["!hide"]}, + "behaviors": {} + }, + "Self-post: Imperative label ('!hide') on quoted post": { + "cfg": "none", + "subject": "post", + "author": "self", + "quoteAuthor": "self", + "labels": {"quotedPost": ["!hide"]}, + "behaviors": { + "embed": {"cause": "label:!hide", "alert": true} + } + }, + "Self-post: Imperative label ('!hide') on quoted author account": { + "cfg": "none", + "subject": "post", + "author": "self", + "quoteAuthor": "self", + "labels": {"quotedAccount": ["!hide"]}, + "behaviors": {} + }, + + "Post with blocked author": { + "cfg": "none", + "subject": "post", + "author": "bob", + "labels": {}, + "behaviors": { + "content": {"cause": "blocking", "filter": true, "blur": true, "noOverride": true}, + "avatar": {"blur": true, "noOverride": true} + } + }, + "Post with blocked quoted author": { + "cfg": "none", + "subject": "post", + "author": "alice", + "quoteAuthor": "bob", + "labels": {}, + "behaviors": { + "content": {"cause": "blocking", "filter": true}, + "embed": {"cause": "blocking", "blur": true, "noOverride": true} + } + }, + + "Post with author blocking user": { + "cfg": "none", + "subject": "post", + "author": "carla", + "labels": {}, + "behaviors": { + "content": {"cause": "blocked-by", "filter": true, "blur": true, "noOverride": true}, + "avatar": {"blur": true, "noOverride": true} + } + }, + "Post with quoted author blocking user": { + "cfg": "none", + "subject": "post", + "author": "alice", + "quoteAuthor": "carla", + "labels": {}, + "behaviors": { + "content": {"cause": "blocked-by", "filter": true}, + "embed": {"cause": "blocked-by", "blur": true, "noOverride": true} + } + }, + + "Post with muted author": { + "cfg": "none", + "subject": "post", + "author": "dan", + "labels": {}, + "behaviors": { + "content": {"cause": "muted", "filter": true, "blur": true} + } + }, + "Post with muted quoted author": { + "cfg": "none", + "subject": "post", + "author": "alice", + "quoteAuthor": "dan", + "labels": {}, + "behaviors": { + "content": {"cause": "muted", "filter": true}, + "embed": {"cause": "muted", "blur": true} + } + }, + + "Post with muted-by-list author": { + "cfg": "none", + "subject": "post", + "author": "elise", + "labels": {}, + "behaviors": { + "content": {"cause": "muted-by-list", "filter": true, "blur": true} + } + }, + "Post with muted-by-list quoted author": { + "cfg": "none", + "subject": "post", + "author": "alice", + "quoteAuthor": "elise", + "labels": {}, + "behaviors": { + "content": {"cause": "muted-by-list", "filter": true}, + "embed": {"cause": "muted-by-list", "blur": true} + } + }, + + "Prioritization: post with blocking & blocked-by author": { + "cfg": "none", + "subject": "post", + "author": "fern", + "labels": {}, + "behaviors": { + "content": {"cause": "blocking", "filter": true, "blur": true, "noOverride": true}, + "avatar": {"blur": true, "noOverride": true} + } + }, + "Prioritization: post with blocking & blocked-by quoted author": { + "cfg": "none", + "subject": "post", + "author": "alice", + "quoteAuthor": "fern", + "labels": {}, + "behaviors": { + "content": {"cause": "blocking", "filter": true}, + "embed": {"cause": "blocking", "blur": true, "noOverride": true} + } + }, + "Prioritization: '!hide' label on post by blocked user": { + "cfg": "none", + "subject": "post", + "author": "bob", + "labels": {"post": ["!hide"]}, + "behaviors": { + "content": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true}, + "avatar": {"blur": true, "noOverride": true} + } + }, + "Prioritization: '!hide' label on quoted post, post by blocked user": { + "cfg": "none", + "subject": "post", + "author": "bob", + "quoteAuthor": "alice", + "labels": {"quotedPost": ["!hide"]}, + "behaviors": { + "content": {"cause": "blocking", "filter": true, "blur": true, "noOverride": true}, + "avatar": {"blur": true, "noOverride": true}, + "embed": {"cause": "label:!hide", "blur": true, "noOverride": true} + } + }, + "Prioritization: '!hide' and 'intolerant' labels on post (hide)": { + "cfg": "intolerant-hide", + "subject": "post", + "author": "alice", + "labels": {"post": ["!hide", "intolerant"]}, + "behaviors": { + "content": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true} + } + }, + "Prioritization: '!warn' and 'intolerant' labels on post (hide)": { + "cfg": "intolerant-hide", + "subject": "post", + "author": "alice", + "labels": {"post": ["!warn", "intolerant"]}, + "behaviors": { + "content": {"cause": "label:intolerant", "filter": true, "blur": true} + } + }, + "Prioritization: '!hide' and 'porn' labels on post (hide)": { + "cfg": "porn-hide", + "subject": "post", + "author": "alice", + "labels": {"post": ["!hide", "porn"]}, + "behaviors": { + "content": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true} + } + }, + "Prioritization: '!warn' and 'porn' labels on post (hide)": { + "cfg": "porn-hide", + "subject": "post", + "author": "alice", + "labels": {"post": ["!warn", "porn"]}, + "behaviors": { + "content": {"cause": "label:porn", "filter": true}, + "embed": {"cause": "label:porn", "blur": true} + } + } + } +} \ No newline at end of file diff --git a/packages/api/definitions/profile-moderation-behaviors.json b/packages/api/definitions/profile-moderation-behaviors.json new file mode 100644 index 00000000000..7db48d8b679 --- /dev/null +++ b/packages/api/definitions/profile-moderation-behaviors.json @@ -0,0 +1,447 @@ + +{ + "users": { + "self": {"blocking": false, "blockedBy": false, "muted": false, "mutedByList": false}, + "alice": {"blocking": false, "blockedBy": false, "muted": false, "mutedByList": false}, + "bob": {"blocking": true, "blockedBy": false, "muted": false, "mutedByList": false}, + "carla": {"blocking": false, "blockedBy": true, "muted": false, "mutedByList": false}, + "dan": {"blocking": false, "blockedBy": false, "muted": true, "mutedByList": false}, + "elise": {"blocking": false, "blockedBy": false, "muted": false, "mutedByList": true}, + "fern": {"blocking": true, "blockedBy": true, "muted": false, "mutedByList": false} + }, + "configurations": { + "none": {}, + "adult-disabled": { + "adultContentEnabled": false + }, + "intolerant-hide": { + "settings": {"intolerant": "hide"} + }, + "intolerant-warn": { + "settings": {"intolerant": "warn"} + }, + "intolerant-ignore": { + "settings": {"intolerant": "ignore"} + }, + "porn-hide": { + "adultContentEnabled": true, + "settings": {"porn": "hide"} + }, + "porn-warn": { + "adultContentEnabled": true, + "settings": {"porn": "warn"} + }, + "porn-ignore": { + "adultContentEnabled": true, + "settings": {"porn": "ignore"} + }, + "scam-hide": { + "settings": {"scam": "hide"} + }, + "scam-warn": { + "settings": {"scam": "warn"} + }, + "scam-ignore": { + "settings": {"scam": "ignore"} + }, + "intolerant-hide-scam-warn": { + "settings": {"intolerant": "hide", "scam": "hide"} + } + }, + "scenarios": { + "Imperative label ('!hide') on account": { + "cfg": "none", + "subject": "profile", + "author": "alice", + "labels": {"account": ["!hide"]}, + "behaviors": { + "account": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true}, + "avatar": {"blur": true, "noOverride": true} + } + }, + "Imperative label ('!hide') on profile": { + "cfg": "none", + "subject": "profile", + "author": "alice", + "labels": {"profile": ["!hide"]}, + "behaviors": { + "profile": {"cause": "label:!hide", "blur": true, "noOverride": true}, + "avatar": {"blur": true, "noOverride": true} + } + }, + + "Imperative label ('!no-promote') on account": { + "cfg": "none", + "subject": "profile", + "author": "alice", + "labels": {"account": ["!no-promote"]}, + "behaviors": { + "account": {"cause": "label:!no-promote", "filter": true} + } + }, + "Imperative label ('!no-promote') on profile": { + "cfg": "none", + "subject": "profile", + "author": "alice", + "labels": {"profile": ["!no-promote"]}, + "behaviors": {} + }, + + "Imperative label ('!warn') on account": { + "cfg": "none", + "subject": "profile", + "author": "alice", + "labels": {"account": ["!warn"]}, + "behaviors": { + "account": {"cause": "label:!warn", "blur": true}, + "avatar": {"blur": true} + } + }, + "Imperative label ('!warn') on profile": { + "cfg": "none", + "subject": "profile", + "author": "alice", + "labels": {"profile": ["!warn"]}, + "behaviors": { + "profile": {"cause": "label:!warn", "blur": true}, + "avatar": {"blur": true} + } + }, + + "Blur label ('intolerant') on account (hide)": { + "cfg": "intolerant-hide", + "subject": "profile", + "author": "alice", + "labels": {"account": ["intolerant"]}, + "behaviors": { + "account": {"cause": "label:intolerant", "filter": true, "blur": true}, + "avatar": {"blur": true} + } + }, + "Blur label ('intolerant') on profile (hide)": { + "cfg": "intolerant-hide", + "subject": "profile", + "author": "alice", + "labels": {"profile": ["intolerant"]}, + "behaviors": { + "profile": {"cause": "label:intolerant", "blur": true}, + "avatar": {"blur": true} + } + }, + + "Blur label ('intolerant') on account (warn)": { + "cfg": "intolerant-warn", + "subject": "profile", + "author": "alice", + "labels": {"account": ["intolerant"]}, + "behaviors": { + "account": {"cause": "label:intolerant", "blur": true}, + "avatar": {"blur": true} + } + }, + "Blur label ('intolerant') on profile (warn)": { + "cfg": "intolerant-warn", + "subject": "profile", + "author": "alice", + "labels": {"profile": ["intolerant"]}, + "behaviors": { + "profile": {"cause": "label:intolerant", "blur": true}, + "avatar": {"blur": true} + } + }, + + "Blur label ('intolerant') on account (ignore)": { + "cfg": "intolerant-ignore", + "subject": "profile", + "author": "alice", + "labels": {"account": ["intolerant"]}, + "behaviors": {} + }, + "Blur label ('intolerant') on profile (ignore)": { + "cfg": "intolerant-ignore", + "subject": "profile", + "author": "alice", + "labels": {"profile": ["intolerant"]}, + "behaviors": {} + }, + + "Blur-media label ('porn') on account (hide)": { + "cfg": "porn-hide", + "subject": "profile", + "author": "alice", + "labels": {"account": ["porn"]}, + "behaviors": { + "account": {"cause": "label:porn", "filter": true, "blur": true}, + "avatar": {"blur": true} + } + }, + "Blur-media label ('porn') on profile (hide)": { + "cfg": "porn-hide", + "subject": "profile", + "author": "alice", + "labels": {"profile": ["porn"]}, + "behaviors": { + "avatar": {"blur": true} + } + }, + + "Blur-media label ('porn') on account (warn)": { + "cfg": "porn-warn", + "subject": "profile", + "author": "alice", + "labels": {"account": ["porn"]}, + "behaviors": { + "account": {"cause": "label:porn", "blur": true}, + "avatar": {"blur": true} + } + }, + "Blur-media label ('porn') on profile (warn)": { + "cfg": "porn-warn", + "subject": "profile", + "author": "alice", + "labels": {"profile": ["porn"]}, + "behaviors": { + "avatar": {"blur": true} + } + }, + + "Blur-media label ('porn') on account (ignore)": { + "cfg": "porn-ignore", + "subject": "profile", + "author": "alice", + "labels": {"account": ["porn"]}, + "behaviors": {} + }, + "Blur-media label ('porn') on profile (ignore)": { + "cfg": "porn-ignore", + "subject": "profile", + "author": "alice", + "labels": {"profile": ["porn"]}, + "behaviors": {} + }, + + "Notice label ('scam') on account (hide)": { + "cfg": "scam-hide", + "subject": "profile", + "author": "alice", + "labels": {"account": ["scam"]}, + "behaviors": { + "account": {"cause": "label:scam", "filter": true, "alert": true}, + "avatar": {"alert": true} + } + }, + "Notice label ('scam') on profile (hide)": { + "cfg": "scam-hide", + "subject": "profile", + "author": "alice", + "labels": {"profile": ["scam"]}, + "behaviors": { + "profile": {"cause": "label:scam", "alert": true}, + "avatar": {"alert": true} + } + }, + + "Notice label ('scam') on account (warn)": { + "cfg": "scam-warn", + "subject": "profile", + "author": "alice", + "labels": {"account": ["scam"]}, + "behaviors": { + "account": {"cause": "label:scam", "alert": true}, + "avatar": {"alert": true} + } + }, + "Notice label ('scam') on profile (warn)": { + "cfg": "scam-warn", + "subject": "profile", + "author": "alice", + "labels": {"profile": ["scam"]}, + "behaviors": { + "profile": {"cause": "label:scam", "alert": true}, + "avatar": {"alert": true} + } + }, + + "Notice label ('scam') on account (ignore)": { + "cfg": "scam-ignore", + "subject": "profile", + "author": "alice", + "labels": {"account": ["scam"]}, + "behaviors": {} + }, + "Notice label ('scam') on profile (ignore)": { + "cfg": "scam-ignore", + "subject": "profile", + "author": "alice", + "labels": {"profile": ["scam"]}, + "behaviors": {} + }, + + "Adult-only label on account when adult content is disabled": { + "cfg": "adult-disabled", + "subject": "profile", + "author": "alice", + "labels": {"account": ["porn"]}, + "behaviors": { + "account": {"cause": "label:porn", "filter": true, "blur": true, "noOverride": true}, + "avatar": {"blur": true, "noOverride": true} + } + }, + "Adult-only label on profile when adult content is disabled": { + "cfg": "adult-disabled", + "subject": "profile", + "author": "alice", + "labels": {"profile": ["porn"]}, + "behaviors": { + "avatar": {"blur": true, "noOverride": true} + } + }, + + "Self-profile: !hide on account": { + "cfg": "none", + "subject": "profile", + "author": "self", + "labels": {"account": ["!hide"]}, + "behaviors": { + "account": {"cause": "label:!hide", "alert": true}, + "avatar": {"alert": true} + } + }, + "Self-profile: !hide on profile": { + "cfg": "none", + "subject": "profile", + "author": "self", + "labels": {"profile": ["!hide"]}, + "behaviors": { + "profile": {"cause": "label:!hide", "alert": true}, + "avatar": {"alert": true} + } + }, + + "Mute/block: Blocking user": { + "cfg": "none", + "subject": "profile", + "author": "bob", + "labels": {}, + "behaviors": { + "account": {"cause": "blocking", "filter": true}, + "avatar": {"blur": true, "noOverride": true} + } + }, + + "Mute/block: Blocked by user": { + "cfg": "none", + "subject": "profile", + "author": "carla", + "labels": {}, + "behaviors": { + "account": {"cause": "blocked-by", "filter": true}, + "avatar": {"blur": true, "noOverride": true} + } + }, + + "Mute/block: Muted user": { + "cfg": "none", + "subject": "profile", + "author": "dan", + "labels": {}, + "behaviors": { + "account": {"cause": "muted", "filter": true} + } + }, + + "Mute/block: Muted-by-list user": { + "cfg": "none", + "subject": "profile", + "author": "elise", + "labels": {}, + "behaviors": { + "account": {"cause": "muted-by-list", "filter": true} + } + }, + + "Prioritization: blocking & blocked-by user": { + "cfg": "none", + "subject": "profile", + "author": "fern", + "labels": {}, + "behaviors": { + "account": {"cause": "blocking", "filter": true, "blur": false}, + "avatar": {"blur": true, "noOverride": true} + } + }, + "Prioritization: '!hide' label on account of blocked user": { + "cfg": "none", + "subject": "profile", + "author": "bob", + "labels": {"account": ["!hide"]}, + "behaviors": { + "account": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true}, + "avatar": {"blur": true, "noOverride": true} + } + }, + "Prioritization: '!hide' and 'intolerant' labels on account (hide)": { + "cfg": "intolerant-hide", + "subject": "profile", + "author": "alice", + "labels": {"account": ["!hide", "intolerant"]}, + "behaviors": { + "account": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true}, + "avatar": {"blur": true, "noOverride": true} + } + }, + "Prioritization: '!warn' and 'intolerant' labels on account (hide)": { + "cfg": "intolerant-hide", + "subject": "profile", + "author": "alice", + "labels": {"account": ["!warn", "intolerant"]}, + "behaviors": { + "account": {"cause": "label:intolerant", "filter": true, "blur": true}, + "avatar": {"blur": true} + } + }, + "Prioritization: '!warn' and 'porn' labels on account (hide)": { + "cfg": "porn-hide", + "subject": "profile", + "author": "alice", + "labels": {"account": ["!warn", "porn"]}, + "behaviors": { + "account": {"cause": "label:porn", "filter": true, "blur": true}, + "avatar": {"blur": true} + } + }, + "Prioritization: intolerant label on account (hide) and scam label on profile (warn)": { + "cfg": "intolerant-hide-scam-warn", + "subject": "profile", + "author": "alice", + "labels": {"account": ["intolerant"], "profile": ["scam"]}, + "behaviors": { + "account": {"cause": "label:intolerant", "filter": true, "blur": true}, + "profile": {"cause": "label:scam", "alert": true}, + "avatar": {"blur": true, "alert": true} + } + }, + "Prioritization: !hide on account, !warn on profile": { + "cfg": "none", + "subject": "profile", + "author": "alice", + "labels": {"account": ["!hide"], "profile": ["!warn"]}, + "behaviors": { + "account": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true}, + "profile": {"cause": "label:!warn", "blur": true}, + "avatar": {"blur": true, "noOverride": true} + } + }, + "Prioritization: !warn on account, !hide on profile": { + "cfg": "none", + "subject": "profile", + "author": "alice", + "labels": {"account": ["!warn"], "profile": ["!hide"]}, + "behaviors": { + "account": {"cause": "label:!warn", "blur": true}, + "profile": {"cause": "label:!hide", "blur": true, "noOverride": true}, + "avatar": {"blur": true, "noOverride": true} + } + } + } +} \ No newline at end of file diff --git a/packages/api/definitions/proposed-labels.json b/packages/api/definitions/proposed-labels.json new file mode 100644 index 00000000000..59cbcb4adfc --- /dev/null +++ b/packages/api/definitions/proposed-labels.json @@ -0,0 +1,326 @@ +[ + { + "id": "system", + "configurable": false, + "labels": [ + { + "id": "!hide", + "preferences": ["hide"], + "flags": ["no-override"], + "onwarn": "blur" + }, + { + "id": "!no-promote", + "preferences": ["hide"], + "flags": [], + "onwarn": null + }, + { + "id": "!warn", + "preferences": ["warn"], + "flags": [], + "onwarn": "blur" + } + ] + }, + { + "id": "legal", + "configurable": false, + "labels": [ + { + "id": "nudity-nonconsensual", + "preferences": ["hide"], + "flags": ["no-override"], + "onwarn": "blur" + }, + { + "id": "dmca-violation", + "preferences": ["hide"], + "flags": ["no-override"], + "onwarn": "blur" + }, + { + "id": "doxxing", + "preferences": ["hide"], + "flags": ["no-override"], + "onwarn": "blur" + } + ] + }, + { + "id": "sexual", + "configurable": true, + "labels": [ + { + "id": "porn", + "preferences": ["ignore", "warn", "hide"], + "flags": ["adult"], + "onwarn": "blur-media" + }, + { + "id": "sexual", + "preferences": ["ignore", "warn", "hide"], + "flags": ["adult"], + "onwarn": "blur-media" + }, + { + "id": "nudity", + "preferences": ["ignore", "warn", "hide"], + "flags": ["adult"], + "onwarn": "blur-media" + } + ] + }, + { + "id": "violence", + "configurable": true, + "labels": [ + { + "id": "nsfl", + "preferences": ["ignore", "warn", "hide"], + "flags": ["adult"], + "onwarn": "blur-media" + }, + { + "id": "corpse", + "preferences": ["ignore", "warn", "hide"], + "flags": ["adult"], + "onwarn": "blur-media" + }, + { + "id": "gore", + "preferences": ["ignore", "warn", "hide"], + "flags": ["adult"], + "onwarn": "blur-media" + }, + { + "id": "torture", + "preferences": ["ignore", "warn", "hide"], + "flags": ["adult"], + "onwarn": "blur" + }, + { + "id": "self-harm", + "preferences": ["ignore", "warn", "hide"], + "flags": ["adult"], + "onwarn": "blur-media" + } + ] + }, + { + "id": "intolerance", + "configurable": true, + "labels": [ + { + "id": "intolerant-race", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "intolerant-gender", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "intolerant-sexual-orientation", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "intolerant-religion", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "intolerant", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "icon-intolerant", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur-media" + } + ] + }, + { + "id": "rude", + "configurable": true, + "labels": [ + { + "id": "trolling", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "harassment", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "bullying", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "threat", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + } + ] + }, + { + "id": "curation", + "configurable": true, + "labels": [ + { + "id": "disgusting", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "upsetting", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "profane", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "politics", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "troubling", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "negative", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "discourse", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "spoiler", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + } + ] + }, + { + "id": "spam", + "configurable": true, + "labels": [ + { + "id": "spam", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "clickbait", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "shill", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "promotion", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + } + ] + }, + { + "id": "misinfo", + "configurable": true, + "labels": [ + { + "id": "account-security", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "net-abuse", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "blur" + }, + { + "id": "impersonation", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "alert" + }, + { + "id": "scam", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "alert" + }, + { + "id": "misinformation", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "alert" + }, + { + "id": "unverified", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "alert" + }, + { + "id": "manipulated", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "alert" + }, + { + "id": "fringe", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "alert" + }, + { + "id": "bullshit", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "alert" + } + ] + } +] \ No newline at end of file diff --git a/packages/api/docs/labels.md b/packages/api/docs/labels.md new file mode 100644 index 00000000000..03f003a85fb --- /dev/null +++ b/packages/api/docs/labels.md @@ -0,0 +1,522 @@ + + + # Labels + + This document is a reference for the labels used in the SDK. + + **⚠️ Note**: These labels are still in development and may change over time. Not all are currently in use. + + ## Key + + ### Label Preferences + + The possible client interpretations for a label. + + - ignore Do nothing with the label. + - warn Provide some form of warning on the content (see "On Warn" behavior). + - hide Remove the content from feeds and apply the warning when directly viewed. + + Each label specifies which preferences it can support. If a label is not configurable, it must have only own supported preference. + + ### Configurable? + + Non-configurable labels cannot have their preference changed by the user. + + ### Flags + + Additional behaviors which a label can adopt. + + - no-override The user cannot click through any covering of content created by the label. + - adult The user must have adult content enabled to configure the label. If adult content is not enabled, the label must adopt the strictest preference. + + ### On Warn + + The kind of UI behavior used when a warning must be applied. + + - blur Hide all of the content behind an interstitial. + - blur-media Hide only the media within the content (ie images) behind an interstitial. + - alert Display a descriptive warning but do not hide the content. + - null Do nothing. + + ## Label Behaviors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDGroupPreferencesConfigurableFlagsOn Warn
!hidesystemhideno-overrideblur
!no-promotesystemhidenull
!warnsystemwarnblur
dmca-violationlegalhideno-overrideblur
doxxinglegalhideno-overrideblur
pornsexualignore, warn, hideadultblur-media
sexualsexualignore, warn, hideadultblur-media
nuditysexualignore, warn, hideadultblur-media
nsflviolenceignore, warn, hideadultblur-media
corpseviolenceignore, warn, hideadultblur-media
goreviolenceignore, warn, hideadultblur-media
tortureviolenceignore, warn, hideadultblur
self-harmviolenceignore, warn, hideadultblur-media
intolerant-raceintoleranceignore, warn, hideblur
intolerant-genderintoleranceignore, warn, hideblur
intolerant-sexual-orientationintoleranceignore, warn, hideblur
intolerant-religionintoleranceignore, warn, hideblur
intolerantintoleranceignore, warn, hideblur
icon-intolerantintoleranceignore, warn, hideblur-media
threatrudeignore, warn, hideblur
spoilercurationignore, warn, hideblur
spamspamignore, warn, hideblur
account-securitymisinfoignore, warn, hideblur
net-abusemisinfoignore, warn, hideblur
impersonationmisinfoignore, warn, hidealert
scammisinfoignore, warn, hidealert
+ + ## Label Group Descriptions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDDescription
systemgeneral
System
Moderator overrides for special cases.
legalgeneral
Legal
Content removed for legal reasons.
sexualgeneral
Adult Content
Content which is sexual in nature.
violencegeneral
Violence
Content which is violent or deeply disturbing.
intolerancegeneral
Intolerance
Content or behavior which is hateful or intolerant toward a group of people.
rudegeneral
Rude
Behavior which is rude toward other users.
curationgeneral
Curational
Subjective moderation geared towards curating a more positive environment.
spamgeneral
Spam
Content which doesn't add to the conversation.
misinfogeneral
Misinformation
Content which misleads or defrauds users.
+ + ## Label Descriptions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDDescription
!hide + general
Moderator Hide
Moderator has chosen to hide the content.

+ on an account
Content Blocked
This account has been hidden by the moderators.

+ on content
Content Blocked
This content has been hidden by the moderators.

+
!no-promote + general
Moderator Filter
Moderator has chosen to filter the content from feeds.

+ on an account
N/A
N/A

+ on content
N/A
N/A

+
!warn + general
Moderator Warn
Moderator has chosen to set a general warning on the content.

+ on an account
Content Warning
This account has received a general warning from moderators.

+ on content
Content Warning
This content has received a general warning from moderators.

+
dmca-violation + general
Copyright Violation
The content has received a DMCA takedown request.

+ on an account
Copyright Violation
This account has received a DMCA takedown request. It will be restored if the concerns can be resolved.

+ on content
Copyright Violation
This content has received a DMCA takedown request. It will be restored if the concerns can be resolved.

+
doxxing + general
Doxxing
Information that reveals private information about someone which has been shared without the consent of the subject.

+ on an account
Doxxing
This account has been reported to publish private information about someone without their consent. This report is currently under review.

+ on content
Doxxing
This content has been reported to include private information about someone without their consent.

+
porn + general
Pornography
Images of full-frontal nudity (genitalia) in any sexualized context, or explicit sexual activity (meaning contact with genitalia or breasts) even if partially covered. Includes graphic sexual cartoons (often jokes/memes).

+ on an account
Pornography
This account contains imagery of full-frontal nudity or explicit sexual activity.

+ on content
Pornography
This content contains imagery of full-frontal nudity or explicit sexual activity.

+
sexual + general
Sexually Suggestive
Content that does not meet the level of "pornography", but is still sexual. Some common examples have been selfies and "hornyposting" with underwear on, or partially naked (naked but covered, eg with hands or from side perspective). Sheer/see-through nipples may end up in this category.

+ on an account
Sexually Suggestive
This account contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress.

+ on content
Sexually Suggestive
This content contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress.

+
nudity + general
Nudity
Nudity which is not sexual, or that is primarily "artistic" in nature. For example: breastfeeding; classic art paintings and sculptures; newspaper images with some nudity; fashion modeling. "Erotic photography" is likely to end up in sexual or porn.

+ on an account
Nudity
This account contains imagery which portrays nudity in a non-sexual or artistic setting.

+ on content
Nudity
This content contains imagery which portrays nudity in a non-sexual or artistic setting.

+
nsfl + general
NSFL
"Not Suitable For Life." This includes graphic images like the infamous "goatse" (don't look it up).

+ on an account
Graphic Imagery (NSFL)
This account contains graphic images which are often referred to as "Not Suitable For Life."

+ on content
Graphic Imagery (NSFL)
This content contains graphic images which are often referred to as "Not Suitable For Life."

+
corpse + general
Corpse
Visual image of a dead human body in any context. Includes war images, hanging, funeral caskets. Does not include all figurative cases (cartoons), but can include realistic figurative images or renderings.

+ on an account
Graphic Imagery (Corpse)
This account contains images of a dead human body in any context. Includes war images, hanging, funeral caskets.

+ on content
Graphic Imagery (Corpse)
This content contains images of a dead human body in any context. Includes war images, hanging, funeral caskets.

+
gore + general
Gore
Intended for shocking images, typically involving blood or visible wounds.

+ on an account
Graphic Imagery (Gore)
This account contains shocking images involving blood or visible wounds.

+ on content
Graphic Imagery (Gore)
This content contains shocking images involving blood or visible wounds.

+
torture + general
Torture
Depictions of torture of a human or animal (animal cruelty).

+ on an account
Graphic Imagery (Torture)
This account contains depictions of torture of a human or animal.

+ on content
Graphic Imagery (Torture)
This content contains depictions of torture of a human or animal.

+
self-harm + general
Self-Harm
A visual depiction (photo or figurative) of cutting, suicide, or similar.

+ on an account
Graphic Imagery (Self-Harm)
This account includes depictions of cutting, suicide, or other forms of self-harm.

+ on content
Graphic Imagery (Self-Harm)
This content includes depictions of cutting, suicide, or other forms of self-harm.

+
intolerant-race + general
Racial Intolerance
Hateful or intolerant content related to race.

+ on an account
Intolerance (Racial)
This account includes hateful or intolerant content related to race.

+ on content
Intolerance (Racial)
This content includes hateful or intolerant views related to race.

+
intolerant-gender + general
Gender Intolerance
Hateful or intolerant content related to gender or gender identity.

+ on an account
Intolerance (Gender)
This account includes hateful or intolerant content related to gender or gender identity.

+ on content
Intolerance (Gender)
This content includes hateful or intolerant views related to gender or gender identity.

+
intolerant-sexual-orientation + general
Sexual Orientation Intolerance
Hateful or intolerant content related to sexual preferences.

+ on an account
Intolerance (Orientation)
This account includes hateful or intolerant content related to sexual preferences.

+ on content
Intolerance (Orientation)
This content includes hateful or intolerant views related to sexual preferences.

+
intolerant-religion + general
Religious Intolerance
Hateful or intolerant content related to religious views or practices.

+ on an account
Intolerance (Religious)
This account includes hateful or intolerant content related to religious views or practices.

+ on content
Intolerance (Religious)
This content includes hateful or intolerant views related to religious views or practices.

+
intolerant + general
Intolerance
A catchall for hateful or intolerant content which is not covered elsewhere.

+ on an account
Intolerance
This account includes hateful or intolerant content.

+ on content
Intolerance
This content includes hateful or intolerant views.

+
icon-intolerant + general
Intolerant Iconography
Visual imagery associated with a hate group, such as the KKK or Nazi, in any context (supportive, critical, documentary, etc).

+ on an account
Intolerant Iconography
This account includes imagery associated with a hate group such as the KKK or Nazis. This warning may apply to content any context, including critical or documentary purposes.

+ on content
Intolerant Iconography
This content includes imagery associated with a hate group such as the KKK or Nazis. This warning may apply to content any context, including critical or documentary purposes.

+
threat + general
Threats
Statements or imagery published with the intent to threaten, intimidate, or harm.

+ on an account
Threats
The moderators believe this account has published statements or imagery with the intent to threaten, intimidate, or harm others.

+ on content
Threats
The moderators believe this content was published with the intent to threaten, intimidate, or harm others.

+
spoiler + general
Spoiler
Discussion about film, TV, etc which gives away plot points.

+ on an account
Spoiler Warning
This account contains discussion about film, TV, etc which gives away plot points.

+ on content
Spoiler Warning
This content contains discussion about film, TV, etc which gives away plot points.

+
spam + general
Spam
Repeat, low-quality messages which are clearly not designed to add to a conversation or space.

+ on an account
Spam
This account publishes repeat, low-quality messages which are clearly not designed to add to a conversation or space.

+ on content
Spam
This content is a part of repeat, low-quality messages which are clearly not designed to add to a conversation or space.

+
account-security + general
Security Concerns
Content designed to hijack user accounts such as a phishing attack.

+ on an account
Security Warning
This account has published content designed to hijack user accounts such as a phishing attack.

+ on content
Security Warning
This content is designed to hijack user accounts such as a phishing attack.

+
net-abuse + general
Network Attacks
Content designed to attack network systems such as denial-of-service attacks.

+ on an account
Network Attack Warning
This account has published content designed to attack network systems such as denial-of-service attacks.

+ on content
Network Attack Warning
This content is designed to attack network systems such as denial-of-service attacks.

+
impersonation + general
Impersonation
Accounts which falsely assert some identity.

+ on an account
Impersonation Warning
The moderators believe this account is lying about their identity.

+ on content
Impersonation Warning
The moderators believe this account is lying about their identity.

+
scam + general
Scam
Fraudulent content.

+ on an account
Scam Warning
The moderators believe this account publishes fraudulent content.

+ on content
Scam Warning
The moderators believe this is fraudulent content.

+
\ No newline at end of file diff --git a/packages/api/docs/moderation-behaviors/posts.md b/packages/api/docs/moderation-behaviors/posts.md new file mode 100644 index 00000000000..9ad384c7400 --- /dev/null +++ b/packages/api/docs/moderation-behaviors/posts.md @@ -0,0 +1,1919 @@ + + +# Post moderation behaviors + +This document is a reference for the expected behaviors for a post in the application based on some given scenarios. The moderatePost() command condense down to the following yes or no decisions: + +- res.content.filter Do not show the post in feeds. +- res.content.blur Put the post behind a warning cover. +- res.content.noOverride Do not allow the post's blur cover to be lifted. +- res.content.alert Add a warning to the post but do not cover it. +- res.avatar.blur Put the avatar behind a cover. +- res.avatar.noOverride Do not allow the avatars's blur cover to be lifted. +- res.avatar.alert Put a warning icon on the avatar. +- res.embed.blur Put the embed content (media, quote post) behind a warning cover. +- res.embed.noOverride Do not allow the embed's blur cover to be lifted. +- res.embed.alert Put a warning on the embed content (media, quote post). + +Key: + +- ❌ = Filter Content +- 🚫 = Blur (no-override) +- ✋ = Blur +- 🪧 = Alert + +## Scenarios + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ScenarioFilterContentAvatarEmbed
Imperative label ('!hide') on post +❌ + +🚫 + + + + + + + +
Imperative label ('!hide') on author profile + + + + + +🚫 + + + + +
Imperative label ('!hide') on author account +❌ + +🚫 + + +🚫 + + + + +
Imperative label ('!hide') on quoted post +❌ + + + + + + + +🚫 + +
Imperative label ('!hide') on quoted author account +❌ + + + + + + + +🚫 + +
Imperative label ('!no-promote') on post +❌ + + + + + + + + + +
Imperative label ('!no-promote') on author profile + + + + + + + + + + +
Imperative label ('!no-promote') on author account +❌ + + + + + + + + + +
Imperative label ('!no-promote') on quoted post +❌ + + + + + + + + + +
Imperative label ('!no-promote') on quoted author account +❌ + + + + + + + + + +
Imperative label ('!warn') on post + + +✋ + + + + + + + +
Imperative label ('!warn') on author profile + + + + + +✋ + + + + +
Imperative label ('!warn') on author account + + +✋ + + +✋ + + + + +
Imperative label ('!warn') on quoted post + + + + + + + + +✋ + +
Imperative label ('!warn') on quoted author account + + + + + + + + +✋ + +
ScenarioFilterContentAvatarEmbed
Blur label ('intolerant') on post (hide) +❌ + +✋ + + + + + + + +
Blur label ('intolerant') on author profile (hide) + + + + + +✋ + + + + +
Blur label ('intolerant') on author account (hide) +❌ + +✋ + + +✋ + + + + +
Blur label ('intolerant') on quoted post (hide) +❌ + + + + + + + +✋ + +
Blur label ('intolerant') on quoted author account (hide) +❌ + + + + + + + +✋ + +
Blur label ('intolerant') on post (warn) + + +✋ + + + + + + + +
Blur label ('intolerant') on author profile (warn) + + + + + +✋ + + + + +
Blur label ('intolerant') on author account (warn) + + +✋ + + +✋ + + + + +
Blur label ('intolerant') on quoted post (warn) + + + + + + + + +✋ + +
Blur label ('intolerant') on quoted author account (warn) + + + + + + + + +✋ + +
Blur label ('intolerant') on post (ignore) + + + + + + + + + + +
Blur label ('intolerant') on author profile (ignore) + + + + + + + + + + +
Blur label ('intolerant') on author account (ignore) + + + + + + + + + + +
Blur label ('intolerant') on quoted post (ignore) + + + + + + + + + + +
Blur label ('intolerant') on quoted author account (ignore) + + + + + + + + + + +
ScenarioFilterContentAvatarEmbed
Blur-media label ('porn') on post (hide) +❌ + + + + + + + +✋ + +
Blur-media label ('porn') on author profile (hide) + + + + + +✋ + + + + +
Blur-media label ('porn') on author account (hide) +❌ + + + + +✋ + + +✋ + +
Blur-media label ('porn') on quoted post (hide) +❌ + + + + + + + + + +
Blur-media label ('porn') on quoted author account (hide) +❌ + + + + + + + + + +
Blur-media label ('porn') on post (warn) + + + + + + + + +✋ + +
Blur-media label ('porn') on author profile (warn) + + + + + +✋ + + + + +
Blur-media label ('porn') on author account (warn) + + + + + +✋ + + +✋ + +
Blur-media label ('porn') on quoted post (warn) + + + + + + + + + + +
Blur-media label ('porn') on quoted author account (warn) + + + + + + + + + + +
Blur-media label ('porn') on post (ignore) + + + + + + + + + + +
Blur-media label ('porn') on author profile (ignore) + + + + + + + + + + +
Blur-media label ('porn') on author account (ignore) + + + + + + + + + + +
Blur-media label ('porn') on quoted post (ignore) + + + + + + + + + + +
Blur-media label ('porn') on quoted author account (ignore) + + + + + + + + + + +
ScenarioFilterContentAvatarEmbed
Notice label ('scam') on post (hide) +❌ + + +🪧 + + + + + + +
Notice label ('scam') on author profile (hide) + + + + + + +🪧 + + + +
Notice label ('scam') on author account (hide) +❌ + + +🪧 + + +🪧 + + + +
Notice label ('scam') on quoted post (hide) +❌ + + + + + + + + +🪧 +
Notice label ('scam') on quoted author account (hide) +❌ + + + + + + + + +🪧 +
Notice label ('scam') on post (warn) + + + +🪧 + + + + + + +
Notice label ('scam') on author profile (warn) + + + + + + +🪧 + + + +
Notice label ('scam') on author account (warn) + + + +🪧 + + +🪧 + + + +
Notice label ('scam') on quoted post (warn) + + + + + + + + + +🪧 +
Notice label ('scam') on quoted author account (warn) + + + + + + + + + +🪧 +
Notice label ('scam') on post (ignore) + + + + + + + + + + +
Notice label ('scam') on author profile (ignore) + + + + + + + + + + +
Notice label ('scam') on author account (ignore) + + + + + + + + + + +
Notice label ('scam') on quoted post (ignore) + + + + + + + + + + +
Notice label ('scam') on quoted author account (ignore) + + + + + + + + + + +
ScenarioFilterContentAvatarEmbed
Adult-only label on post when adult content is disabled +❌ + + + + + + + +🚫 + +
Adult-only label on author profile when adult content is disabled + + + + + +🚫 + + + + +
Adult-only label on author account when adult content is disabled +❌ + + + + +🚫 + + +🚫 + +
Adult-only label on quoted post when adult content is disabled +❌ + + + + + + + + + +
Adult-only label on quoted author account when adult content is disabled +❌ + + + + + + + + + +
ScenarioFilterContentAvatarEmbed
Self-post: Imperative label ('!hide') on post + + + +🪧 + + + + + + +
Self-post: Imperative label ('!hide') on author profile + + + + + + + + + + +
Self-post: Imperative label ('!hide') on author account + + + + + + + + + + +
Self-post: Imperative label ('!hide') on quoted post + + + + + + + + + +🪧 +
Self-post: Imperative label ('!hide') on quoted author account + + + + + + + + + + +
ScenarioFilterContentAvatarEmbed
Post with blocked author +❌ + +🚫 + + +🚫 + + + + +
Post with blocked quoted author +❌ + + + + + + + +🚫 + +
Post with author blocking user +❌ + +🚫 + + +🚫 + + + + +
Post with quoted author blocking user +❌ + + + + + + + +🚫 + +
Post with muted author +❌ + +✋ + + + + + + + +
Post with muted quoted author +❌ + + + + + + + +✋ + +
Post with muted-by-list author +❌ + +✋ + + + + + + + +
Post with muted-by-list quoted author +❌ + + + + + + + +✋ + +
ScenarioFilterContentAvatarEmbed
Prioritization: post with blocking & blocked-by author +❌ + +🚫 + + +🚫 + + + + +
Prioritization: post with blocking & blocked-by quoted author +❌ + + + + + + + +🚫 + +
Prioritization: '!hide' label on post by blocked user +❌ + +🚫 + + +🚫 + + + + +
Prioritization: '!hide' label on quoted post, post by blocked user +❌ + +🚫 + + +🚫 + + +🚫 + +
Prioritization: '!hide' and 'intolerant' labels on post (hide) +❌ + +🚫 + + + + + + + +
Prioritization: '!warn' and 'intolerant' labels on post (hide) +❌ + +✋ + + + + + + + +
Prioritization: '!hide' and 'porn' labels on post (hide) +❌ + +🚫 + + + + + + + +
Prioritization: '!warn' and 'porn' labels on post (hide) +❌ + + + + + + + +✋ + +
\ No newline at end of file diff --git a/packages/api/docs/moderation-behaviors/profiles.md b/packages/api/docs/moderation-behaviors/profiles.md new file mode 100644 index 00000000000..3d2f9af96b3 --- /dev/null +++ b/packages/api/docs/moderation-behaviors/profiles.md @@ -0,0 +1,907 @@ + + +# Profile moderation behaviors + +This document is a reference for the expected behaviors for a profile in the application based on some given scenarios. The moderateProfile() command condense down to the following yes or no decisions: + +- res.account.filter Do not show the account in feeds. +- res.account.blur Put the account (in listings, when viewing) behind a warning cover. +- res.account.noOverride Do not allow the account's blur cover to be lifted. +- res.account.alert Add a warning to the account but do not cover it. +- res.profile.blur Put the profile details (handle, display name, bio) behind a warning cover. +- res.profile.noOverride Do not allow the profile's blur cover to be lifted. +- res.profile.alert Add a warning to the profile but do not cover it. +- res.avatar.blur Put the avatar behind a cover. +- res.avatar.noOverride Do not allow the avatars's blur cover to be lifted. +- res.avatar.alert Put a warning icon on the avatar. + +Key: + +- ❌ = Filter Content +- 🚫 = Blur (no-override) +- ✋ = Blur +- 🪧 = Alert + +## Scenarios + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ScenarioFilterAccountProfileAvatar
Imperative label ('!hide') on account +❌ + +🚫 + + + + + +🚫 + +
Imperative label ('!hide') on profile + + + + + +🚫 + + +🚫 + +
Imperative label ('!no-promote') on account +❌ + + + + + + + + + +
Imperative label ('!no-promote') on profile + + + + + + + + + + +
Imperative label ('!warn') on account + + +✋ + + + + + +✋ + +
Imperative label ('!warn') on profile + + + + + +✋ + + +✋ + +
ScenarioFilterAccountProfileAvatar
Blur label ('intolerant') on account (hide) +❌ + +✋ + + + + + +✋ + +
Blur label ('intolerant') on profile (hide) + + + + + +✋ + + +✋ + +
Blur label ('intolerant') on account (warn) + + +✋ + + + + + +✋ + +
Blur label ('intolerant') on profile (warn) + + + + + +✋ + + +✋ + +
Blur label ('intolerant') on account (ignore) + + + + + + + + + + +
Blur label ('intolerant') on profile (ignore) + + + + + + + + + + +
ScenarioFilterAccountProfileAvatar
Blur-media label ('porn') on account (hide) +❌ + +✋ + + + + + +✋ + +
Blur-media label ('porn') on profile (hide) + + + + + + + + +✋ + +
Blur-media label ('porn') on account (warn) + + +✋ + + + + + +✋ + +
Blur-media label ('porn') on profile (warn) + + + + + + + + +✋ + +
Blur-media label ('porn') on account (ignore) + + + + + + + + + + +
Blur-media label ('porn') on profile (ignore) + + + + + + + + + + +
ScenarioFilterAccountProfileAvatar
Notice label ('scam') on account (hide) +❌ + + +🪧 + + + + + +🪧 +
Notice label ('scam') on profile (hide) + + + + + + +🪧 + + +🪧 +
Notice label ('scam') on account (warn) + + + +🪧 + + + + + +🪧 +
Notice label ('scam') on profile (warn) + + + + + + +🪧 + + +🪧 +
Notice label ('scam') on account (ignore) + + + + + + + + + + +
Notice label ('scam') on profile (ignore) + + + + + + + + + + +
ScenarioFilterAccountProfileAvatar
Adult-only label on account when adult content is disabled +❌ + +🚫 + + + + + +🚫 + +
Adult-only label on profile when adult content is disabled + + + + + + + + +🚫 + +
ScenarioFilterAccountProfileAvatar
Self-profile: !hide on account + + + +🪧 + + + + + +🪧 +
Self-profile: !hide on profile + + + + + + +🪧 + + +🪧 +
ScenarioFilterAccountProfileAvatar
Mute/block: Blocking user +❌ + + + + + + + +🚫 + +
Mute/block: Blocked by user +❌ + + + + + + + +🚫 + +
Mute/block: Muted user +❌ + + + + + + + + + +
Mute/block: Muted-by-list user +❌ + + + + + + + + + +
ScenarioFilterAccountProfileAvatar
Prioritization: blocking & blocked-by user +❌ + + + + + + + +🚫 + +
Prioritization: '!hide' label on account of blocked user +❌ + +🚫 + + + + + +🚫 + +
Prioritization: '!hide' and 'intolerant' labels on account (hide) +❌ + +🚫 + + + + + +🚫 + +
Prioritization: '!warn' and 'intolerant' labels on account (hide) +❌ + +✋ + + + + + +✋ + +
Prioritization: '!warn' and 'porn' labels on account (hide) +❌ + +✋ + + + + + +✋ + +
Prioritization: intolerant label on account (hide) and scam label on profile (warn) +❌ + +✋ + + + +🪧 + +✋ +🪧 +
Prioritization: !hide on account, !warn on profile +❌ + +🚫 + + +✋ + + +🚫 + +
Prioritization: !warn on account, !hide on profile + + +✋ + + +🚫 + + +🚫 + +
\ No newline at end of file diff --git a/packages/api/docs/moderation.md b/packages/api/docs/moderation.md new file mode 100644 index 00000000000..1ebfe3eed77 --- /dev/null +++ b/packages/api/docs/moderation.md @@ -0,0 +1,144 @@ +# Moderation API + +Applying the moderation system is a challenging task, but we've done our best to simplify it for you. The Moderation API helps handle a wide range of tasks, including: + +- User muting (including mutelists) +- User blocking +- Moderator labeling + +For more information, see the [Moderation Documentation](./docs/moderation.md) or the associated [Labels Reference](./docs/labels.md). + +Additional docs: + +- [Labels Reference](./labels.md) +- [Post Moderation Behaviors](./moderation-behaviors/posts.md) +- [Profile Moderation Behaviors](./moderation-behaviors/profiles.md) + +## Configuration + +Every moderation function takes a set of options which look like this: + +```typescript +{ + // the logged-in user's DID + userDid: 'did:plc:1234...', + + // is adult content allowed? + adultContentEnabled: true, + + // the user's labeler settings + labelerSettings: [ + { + labeler: { + did: '...', + displayName: 'My mod service' + }, + settings: { + porn: 'hide', + sexual: 'warn', + nudity: 'ignore', + // ... + } + } + ] +} +``` + +This should match the following interfaces: + +```typescript +interface ModerationOpts { + userDid: string + adultContentEnabled: boolean + labelerSettings: LabelerSettings[] +} + +interface Labeler { + did: string + displayName: string +} + +type LabelPreference = 'ignore' | 'warn' | 'hide' + +interface LabelerSettings { + labeler: Labeler + settings: Record +} +``` + +## Posts + +Applications need to produce the [Post Moderation Behaviors](./moderation-behaviors/posts.md) using the `moderatePost()` API. + +```typescript +import {moderatePost} from '@atproto/api' + +const postMod = moderatePost(postView, getOpts()) + +if (postMod.content.filter) { + // dont render in feeds or similar + // in contexts where this is disruptive (eg threads) you should ignore this and instead check blur +} +if (postMod.content.blur) { + // render the whole object behind a cover (use postMod.content.cause to explain) + if (postMod.content.noOverride) { + // do not allow the cover the be removed + } +} +if (postMod.content.alert) { + // render a warning on the content (use postMod.content.cause to explain) +} +if (postMod.embed.blur) { + // render the embedded media behind a cover (use postMod.embed.cause to explain) + if (postMod.embed.noOverride) { + // do not allow the cover the be removed + } +} +if (postMod.embed.alert) { + // render a warning on the embedded media (use postMod.embed.cause to explain) +} +if (postMod.avatar.blur) { + // render the avatar behind a cover +} +if (postMod.avatar.alert) { + // render an alert on the avatar +} +``` + +## Profiles + +Applications need to produce the [Profile Moderation Behaviors](./moderation-behaviors/profiles.md) using the `moderateProfile()` API. + +```typescript +import {moderateProfile} from '@atproto/api' + +const profileMod = moderateProfile(profileView, getOpts()) + +if (profileMod.acount.filter) { + // dont render in discovery +} +if (profileMod.account.blur) { + // render the whole account behind a cover (use profileMod.account.cause to explain) + if (profileMod.account.noOverride) { + // do not allow the cover the be removed + } +} +if (profileMod.account.alert) { + // render a warning on the account (use profileMod.account.cause to explain) +} +if (profileMod.profile.blur) { + // render the profile information (display name, bio) behind a cover + if (profileMod.profile.noOverride) { + // do not allow the cover the be removed + } +} +if (profileMod.profile.alert) { + // render a warning on the profile (use profileMod.profile.cause to explain) +} +if (profileMod.avatar.blur) { + // render the avatar behind a cover +} +if (profileMod.avatar.alert) { + // render an alert on the avatar +} +``` \ No newline at end of file diff --git a/packages/api/package.json b/packages/api/package.json index 7871a04bf38..13708f98b50 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -3,7 +3,8 @@ "version": "0.4.4", "main": "src/index.ts", "scripts": { - "codegen": "lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", + "codegen": "yarn docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", + "docgen": "node ./scripts/generate-docs.mjs", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", @@ -29,6 +30,7 @@ }, "devDependencies": { "@atproto/lex-cli": "*", - "@atproto/pds": "*" + "@atproto/pds": "*", + "common-tags": "^1.8.2" } } diff --git a/packages/api/scripts/code/label-groups.mjs b/packages/api/scripts/code/label-groups.mjs new file mode 100644 index 00000000000..2c62cbb10b7 --- /dev/null +++ b/packages/api/scripts/code/label-groups.mjs @@ -0,0 +1,68 @@ +import * as url from 'url' +import { readFileSync, writeFileSync } from 'fs' +import { join } from 'path' +import * as prettier from 'prettier' + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) + +const labelsDef = JSON.parse( + readFileSync( + join(__dirname, '..', '..', 'definitions', 'labels.json'), + 'utf8', + ), +) +const labelGroupsEn = JSON.parse( + readFileSync( + join( + __dirname, + '..', + '..', + 'definitions', + 'locale', + 'en', + 'label-groups.json', + ), + 'utf8', + ), +) + +writeFileSync( + join(__dirname, '..', '..', 'src', 'moderation', 'const', 'label-groups.ts'), + await gen(), + 'utf8', +) + +async function gen() { + return prettier.format( + `/** this doc is generated by ./scripts/code/labels.mjs **/ + import {LabelGroupDefinitionMap} from '../types' + import {LABELS} from './labels' + + export const LABEL_GROUPS: LabelGroupDefinitionMap = { + ${genDefMap()} + } + `, + { semi: false, parser: 'babel', singleQuote: true }, + ) +} + +function genDefMap() { + const lines = [] + for (const group of labelsDef) { + lines.push(`"${group.id}": {`) + lines.push(` id: "${group.id}",`) + lines.push(` configurable: ${group.configurable ? true : false},`) + lines.push( + ` labels: [${group.labels + .map((label) => `LABELS["${label.id}"]`) + .join(', ')}],`, + ) + lines.push( + ` strings: {settings: {en: ${JSON.stringify(labelGroupsEn[group.id])}}}`, + ) + lines.push(`},`) + } + return lines.join('\n') +} + +export {} diff --git a/packages/api/scripts/code/labels.mjs b/packages/api/scripts/code/labels.mjs new file mode 100644 index 00000000000..9880afab1ad --- /dev/null +++ b/packages/api/scripts/code/labels.mjs @@ -0,0 +1,68 @@ +import * as url from 'url' +import { readFileSync, writeFileSync } from 'fs' +import { join } from 'path' +import * as prettier from 'prettier' + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) + +const labelsDef = JSON.parse( + readFileSync( + join(__dirname, '..', '..', 'definitions', 'labels.json'), + 'utf8', + ), +) +const labelsEn = JSON.parse( + readFileSync( + join(__dirname, '..', '..', 'definitions', 'locale', 'en', 'labels.json'), + 'utf8', + ), +) + +writeFileSync( + join(__dirname, '..', '..', 'src', 'moderation', 'const', 'labels.ts'), + await gen(), + 'utf8', +) + +async function gen() { + return prettier.format( + `/** this doc is generated by ./scripts/code/labels.mjs **/ + import {LabelDefinitionMap} from '../types' + + export const LABELS: LabelDefinitionMap = ${JSON.stringify( + genDefMap(), + null, + 2, + )} + `, + { semi: false, parser: 'babel', singleQuote: true }, + ) +} + +function genDefMap() { + const labels = {} + for (const group of labelsDef) { + for (const label of group.labels) { + labels[label.id] = { + ...label, + groupId: group.id, + configurable: group.configurable, + strings: { + settings: getLabelStrings(label.id, 'settings'), + account: getLabelStrings(label.id, 'account'), + content: getLabelStrings(label.id, 'content'), + }, + } + } + } + return labels +} + +function getLabelStrings(id, type) { + if (labelsEn[id] && labelsEn[id][type]) { + return { en: labelsEn[id][type] } + } + throw new Error('Label strings not found for ' + id) +} + +export {} diff --git a/packages/api/scripts/docs/labels.mjs b/packages/api/scripts/docs/labels.mjs new file mode 100644 index 00000000000..979b23738e1 --- /dev/null +++ b/packages/api/scripts/docs/labels.mjs @@ -0,0 +1,164 @@ +import * as url from 'url' +import { readFileSync, writeFileSync } from 'fs' +import { join } from 'path' +import { stripIndent } from 'common-tags' + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) + +const labelsDef = JSON.parse( + readFileSync( + join(__dirname, '..', '..', 'definitions', 'labels.json'), + 'utf8', + ), +) +const labelGroupsEn = JSON.parse( + readFileSync( + join( + __dirname, + '..', + '..', + 'definitions', + 'locale', + 'en', + 'label-groups.json', + ), + 'utf8', + ), +) +const labelsEn = JSON.parse( + readFileSync( + join(__dirname, '..', '..', 'definitions', 'locale', 'en', 'labels.json'), + 'utf8', + ), +) + +writeFileSync(join(__dirname, '..', '..', 'docs', 'labels.md'), doc(), 'utf8') + +function doc() { + return stripIndent` + + + # Labels + + This document is a reference for the labels used in the SDK. + + **⚠️ Note**: These labels are still in development and may change over time. Not all are currently in use. + + ## Key + + ### Label Preferences + + The possible client interpretations for a label. + + - ignore Do nothing with the label. + - warn Provide some form of warning on the content (see "On Warn" behavior). + - hide Remove the content from feeds and apply the warning when directly viewed. + + Each label specifies which preferences it can support. If a label is not configurable, it must have only own supported preference. + + ### Configurable? + + Non-configurable labels cannot have their preference changed by the user. + + ### Flags + + Additional behaviors which a label can adopt. + + - no-override The user cannot click through any covering of content created by the label. + - adult The user must have adult content enabled to configure the label. If adult content is not enabled, the label must adopt the strictest preference. + + ### On Warn + + The kind of UI behavior used when a warning must be applied. + + - blur Hide all of the content behind an interstitial. + - blur-media Hide only the media within the content (ie images) behind an interstitial. + - alert Display a descriptive warning but do not hide the content. + - null Do nothing. + + ## Label Behaviors + + + + + + + + + + + ${labelsRef()} +
IDGroupPreferencesConfigurableFlagsOn Warn
+ + ## Label Group Descriptions + + + + + + + ${labelGroupsDesc()} +
IDDescription
+ + ## Label Descriptions + + + + + + + ${labelsDesc()} +
IDDescription
+ ` +} + +function labelsRef() { + const lines = [] + for (const group of labelsDef) { + for (const label of group.labels) { + lines.push(stripIndent` + + ${label.id} + ${group.id} + ${label.preferences.join(', ')} + ${group.configurable ? '✅' : '❌'} + ${label.flags.join(', ')} + ${label.onwarn} + + `) + } + } + return lines.join('\n') +} + +function labelGroupsDesc() { + const lines = [] + for (const id in labelGroupsEn) { + lines.push(stripIndent` + + ${id} + general
${labelGroupsEn[id].name}
${labelGroupsEn[id].description} + + `) + } + return lines.join('\n') +} + +function labelsDesc() { + const lines = [] + for (const id in labelsEn) { + lines.push(stripIndent` + + ${id} + + general
${labelsEn[id].settings.name}
${labelsEn[id].settings.description}

+ on an account
${labelsEn[id].account.name}
${labelsEn[id].account.description}

+ on content
${labelsEn[id].content.name}
${labelsEn[id].content.description}

+ + + `) + } + return lines.join('\n') +} + +export {} diff --git a/packages/api/scripts/docs/post-moderation-behaviors.mjs b/packages/api/scripts/docs/post-moderation-behaviors.mjs new file mode 100644 index 00000000000..e90809fb000 --- /dev/null +++ b/packages/api/scripts/docs/post-moderation-behaviors.mjs @@ -0,0 +1,122 @@ +import * as url from 'url' +import { readFileSync, writeFileSync } from 'fs' +import { join } from 'path' +import { stripIndents } from 'common-tags' + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) + +const postModerationBehaviorsDef = JSON.parse( + readFileSync( + join( + __dirname, + '..', + '..', + 'definitions', + 'post-moderation-behaviors.json', + ), + 'utf8', + ), +) + +writeFileSync( + join(__dirname, '..', '..', 'docs', 'moderation-behaviors', 'posts.md'), + posts(), + 'utf8', +) + +function posts() { + let lastTitle = 'NULL' + return stripIndents` + + + # Post moderation behaviors + + This document is a reference for the expected behaviors for a post in the application based on some given scenarios. The moderatePost() command condense down to the following yes or no decisions: + + - res.content.filter Do not show the post in feeds. + - res.content.blur Put the post behind a warning cover. + - res.content.noOverride Do not allow the post's blur cover to be lifted. + - res.content.alert Add a warning to the post but do not cover it. + - res.avatar.blur Put the avatar behind a cover. + - res.avatar.noOverride Do not allow the avatars's blur cover to be lifted. + - res.avatar.alert Put a warning icon on the avatar. + - res.embed.blur Put the embed content (media, quote post) behind a warning cover. + - res.embed.noOverride Do not allow the embed's blur cover to be lifted. + - res.embed.alert Put a warning on the embed content (media, quote post). + + Key: + + - ❌ = Filter Content + - 🚫 = Blur (no-override) + - ✋ = Blur + - 🪧 = Alert + + ## Scenarios + + + ${Array.from(Object.entries(postModerationBehaviorsDef.scenarios)) + .map(([title, scenario], i) => { + const str = ` + ${title.indexOf(lastTitle) === -1 ? postTableHead() : ''} + ${scenarioSection(title, scenario)} + ` + lastTitle = title.slice(0, 10) + return str + }) + .join('\n\n')} +
+ ` +} + +function postTableHead() { + return `ScenarioFilterContentAvatarEmbed` +} + +function scenarioSection(title, scenario) { + return stripIndents` + + ${title} + + ${filter(scenario.behaviors.content?.filter)} + + + ${blur( + scenario.behaviors.content?.blur, + scenario.behaviors.content?.noOverride, + )} + ${alert(scenario.behaviors.content?.alert)} + + + ${blur( + scenario.behaviors.avatar?.blur, + scenario.behaviors.avatar?.noOverride, + )} + ${alert(scenario.behaviors.avatar?.alert)} + + + ${blur( + scenario.behaviors.embed?.blur, + scenario.behaviors.embed?.noOverride, + )} + ${alert(scenario.behaviors.embed?.alert)} + + + ` +} + +function filter(val) { + return val ? '❌' : '' +} + +function blur(val, noOverride) { + if (val) { + return noOverride ? '🚫' : '✋' + } + return '' +} + +function alert(val) { + return val ? '🪧' : '' +} + +export {} diff --git a/packages/api/scripts/docs/profile-moderation-behaviors.mjs b/packages/api/scripts/docs/profile-moderation-behaviors.mjs new file mode 100644 index 00000000000..413d2593011 --- /dev/null +++ b/packages/api/scripts/docs/profile-moderation-behaviors.mjs @@ -0,0 +1,122 @@ +import * as url from 'url' +import { readFileSync, writeFileSync } from 'fs' +import { join } from 'path' +import { stripIndents } from 'common-tags' + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) + +const profileModerationBehaviorsDef = JSON.parse( + readFileSync( + join( + __dirname, + '..', + '..', + 'definitions', + 'profile-moderation-behaviors.json', + ), + 'utf8', + ), +) + +writeFileSync( + join(__dirname, '..', '..', 'docs', 'moderation-behaviors', 'profiles.md'), + profiles(), + 'utf8', +) + +function profiles() { + let lastTitle = 'NULL' + return stripIndents` + + + # Profile moderation behaviors + + This document is a reference for the expected behaviors for a profile in the application based on some given scenarios. The moderateProfile() command condense down to the following yes or no decisions: + + - res.account.filter Do not show the account in feeds. + - res.account.blur Put the account (in listings, when viewing) behind a warning cover. + - res.account.noOverride Do not allow the account's blur cover to be lifted. + - res.account.alert Add a warning to the account but do not cover it. + - res.profile.blur Put the profile details (handle, display name, bio) behind a warning cover. + - res.profile.noOverride Do not allow the profile's blur cover to be lifted. + - res.profile.alert Add a warning to the profile but do not cover it. + - res.avatar.blur Put the avatar behind a cover. + - res.avatar.noOverride Do not allow the avatars's blur cover to be lifted. + - res.avatar.alert Put a warning icon on the avatar. + + Key: + + - ❌ = Filter Content + - 🚫 = Blur (no-override) + - ✋ = Blur + - 🪧 = Alert + + ## Scenarios + + + ${Array.from(Object.entries(profileModerationBehaviorsDef.scenarios)) + .map(([title, scenario], i) => { + const str = ` + ${title.indexOf(lastTitle) === -1 ? postTableHead() : ''} + ${scenarioSection(title, scenario)} + ` + lastTitle = title.slice(0, 10) + return str + }) + .join('\n\n')} +
+ ` +} + +function postTableHead() { + return `ScenarioFilterAccountProfileAvatar` +} + +function scenarioSection(title, scenario) { + return stripIndents` + + ${title} + + ${filter(scenario.behaviors.account?.filter)} + + + ${blur( + scenario.behaviors.account?.blur, + scenario.behaviors.account?.noOverride, + )} + ${alert(scenario.behaviors.account?.alert)} + + + ${blur( + scenario.behaviors.profile?.blur, + scenario.behaviors.profile?.noOverride, + )} + ${alert(scenario.behaviors.profile?.alert)} + + + ${blur( + scenario.behaviors.avatar?.blur, + scenario.behaviors.avatar?.noOverride, + )} + ${alert(scenario.behaviors.avatar?.alert)} + + + ` +} + +function filter(val) { + return val ? '❌' : '' +} + +function blur(val, noOverride) { + if (val) { + return noOverride ? '🚫' : '✋' + } + return '' +} + +function alert(val) { + return val ? '🪧' : '' +} + +export {} diff --git a/packages/api/scripts/generate-code.mjs b/packages/api/scripts/generate-code.mjs new file mode 100644 index 00000000000..287d9beb393 --- /dev/null +++ b/packages/api/scripts/generate-code.mjs @@ -0,0 +1,4 @@ +import './code/labels.mjs' +import './code/label-groups.mjs' + +export {} diff --git a/packages/api/scripts/generate-docs.mjs b/packages/api/scripts/generate-docs.mjs new file mode 100644 index 00000000000..f7d4a1b2424 --- /dev/null +++ b/packages/api/scripts/generate-docs.mjs @@ -0,0 +1,5 @@ +import './docs/labels.mjs' +import './docs/post-moderation-behaviors.mjs' +import './docs/profile-moderation-behaviors.mjs' + +export {} diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 81297f246b4..33b48a2cf40 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -13,5 +13,9 @@ export * from './agent' export * from './rich-text/rich-text' export * from './rich-text/sanitization' export * from './rich-text/unicode' +export * from './moderation' +export * from './moderation/types' +export { LABELS } from './moderation/const/labels' +export { LABEL_GROUPS } from './moderation/const/label-groups' export { BskyAgent } from './bsky-agent' export { AtpAgent as default } from './agent' diff --git a/packages/api/src/moderation/accumulator.ts b/packages/api/src/moderation/accumulator.ts new file mode 100644 index 00000000000..ed6c06d0ddc --- /dev/null +++ b/packages/api/src/moderation/accumulator.ts @@ -0,0 +1,181 @@ +import { AppBskyGraphDefs } from '../client/index' +import { + Label, + LabelPreference, + ModerationCause, + ModerationOpts, + ModerationDecision, +} from './types' +import { LABELS } from './const/labels' + +export class ModerationCauseAccumulator { + did = '' + causes: ModerationCause[] = [] + + constructor() {} + + setDid(did: string) { + this.did = did + } + + addBlocking(blocking: string | undefined) { + if (blocking) { + this.causes.push({ + type: 'blocking', + source: { type: 'user' }, + priority: 3, + }) + } + } + + addBlockedBy(blockedBy: boolean | undefined) { + if (blockedBy) { + this.causes.push({ + type: 'blocked-by', + source: { type: 'user' }, + priority: 4, + }) + } + } + + addLabel(label: Label, opts: ModerationOpts) { + // look up the label definition + const labelDef = LABELS[label.val] + if (!labelDef) { + // ignore labels we don't understand + return + } + + // look up the label preference + // TODO use the find() when 3P labelers support lands + // const labelerSettings = opts.labelerSettings.find( + // (s) => s.labeler.did === label.src, + // ) + const labelerSettings = opts.labelerSettings[0] + if (!labelerSettings) { + // ignore labels from labelers we don't use + return + } + + // establish the label preference for interpretation + let labelPref: LabelPreference = 'ignore' + if (!labelDef.configurable) { + labelPref = labelDef.preferences[0] + } else if (labelDef.flags.includes('adult') && !opts.adultContentEnabled) { + labelPref = 'hide' + } else if (labelerSettings.settings[label.val]) { + labelPref = labelerSettings.settings[label.val] + } + + // ignore labels the user has asked to ignore + if (labelPref === 'ignore') { + return + } + + // establish the priority of the label + let priority: 1 | 2 | 5 | 7 | 8 + if (labelDef.flags.includes('no-override')) { + priority = 1 + } else if (labelPref === 'hide') { + priority = 2 + } else if (labelDef.onwarn === 'blur') { + priority = 5 + } else if (labelDef.onwarn === 'blur-media') { + priority = 7 + } else { + priority = 8 + } + + this.causes.push({ + type: 'label', + label, + labelDef, + labeler: labelerSettings.labeler, + setting: labelPref, + priority, + }) + } + + addMuted(muted: boolean | undefined) { + if (muted) { + this.causes.push({ + type: 'muted', + source: { type: 'user' }, + priority: 6, + }) + } + } + + addMutedByList(mutedByList: AppBskyGraphDefs.ListViewBasic | undefined) { + if (mutedByList) { + this.causes.push({ + type: 'muted', + source: { type: 'list', list: mutedByList }, + priority: 6, + }) + } + } + + finalizeDecision(opts: ModerationOpts): ModerationDecision { + const mod = new ModerationDecision() + mod.did = this.did + if (!this.causes.length) { + return mod + } + + // sort the causes by priority and then choose the top one + this.causes.sort((a, b) => a.priority - b.priority) + mod.cause = this.causes[0] + mod.additionalCauses = this.causes.slice(1) + + // blocked user + if (mod.cause.type === 'blocking' || mod.cause.type === 'blocked-by') { + // filter and blur, dont allow override + mod.filter = true + mod.blur = true + mod.noOverride = true + } + // muted user + else if (mod.cause.type === 'muted') { + // filter and blur + mod.filter = true + mod.blur = true + } + // labeled subject + else if (mod.cause.type === 'label') { + // 'hide' setting + if (mod.cause.setting === 'hide') { + // filter + mod.filter = true + } + + // 'hide' and 'warn' setting, apply onwarn + switch (mod.cause.labelDef.onwarn) { + case 'alert': + mod.alert = true + break + case 'blur': + mod.blur = true + break + case 'blur-media': + mod.blurMedia = true + break + case null: + // do nothing + break + } + + // apply noOverride as needed + if (mod.cause.labelDef.flags.includes('no-override')) { + mod.noOverride = true + } else if ( + mod.cause.labelDef.flags.includes('adult') && + !opts.adultContentEnabled + ) { + mod.noOverride = true + } + } + + return mod + } +} diff --git a/packages/api/src/moderation/const/label-groups.ts b/packages/api/src/moderation/const/label-groups.ts new file mode 100644 index 00000000000..d142dfa817e --- /dev/null +++ b/packages/api/src/moderation/const/label-groups.ts @@ -0,0 +1,143 @@ +/** this doc is generated by ./scripts/code/labels.mjs **/ +import { LabelGroupDefinitionMap } from '../types' +import { LABELS } from './labels' + +export const LABEL_GROUPS: LabelGroupDefinitionMap = { + system: { + id: 'system', + configurable: false, + labels: [LABELS['!hide'], LABELS['!no-promote'], LABELS['!warn']], + strings: { + settings: { + en: { + name: 'System', + description: 'Moderator overrides for special cases.', + }, + }, + }, + }, + legal: { + id: 'legal', + configurable: false, + labels: [LABELS['dmca-violation'], LABELS['doxxing']], + strings: { + settings: { + en: { + name: 'Legal', + description: 'Content removed for legal reasons.', + }, + }, + }, + }, + sexual: { + id: 'sexual', + configurable: true, + labels: [LABELS['porn'], LABELS['sexual'], LABELS['nudity']], + strings: { + settings: { + en: { + name: 'Adult Content', + description: 'Content which is sexual in nature.', + }, + }, + }, + }, + violence: { + id: 'violence', + configurable: true, + labels: [ + LABELS['nsfl'], + LABELS['corpse'], + LABELS['gore'], + LABELS['torture'], + LABELS['self-harm'], + ], + strings: { + settings: { + en: { + name: 'Violence', + description: 'Content which is violent or deeply disturbing.', + }, + }, + }, + }, + intolerance: { + id: 'intolerance', + configurable: true, + labels: [ + LABELS['intolerant-race'], + LABELS['intolerant-gender'], + LABELS['intolerant-sexual-orientation'], + LABELS['intolerant-religion'], + LABELS['intolerant'], + LABELS['icon-intolerant'], + ], + strings: { + settings: { + en: { + name: 'Intolerance', + description: + 'Content or behavior which is hateful or intolerant toward a group of people.', + }, + }, + }, + }, + rude: { + id: 'rude', + configurable: true, + labels: [LABELS['threat']], + strings: { + settings: { + en: { + name: 'Rude', + description: 'Behavior which is rude toward other users.', + }, + }, + }, + }, + curation: { + id: 'curation', + configurable: true, + labels: [LABELS['spoiler']], + strings: { + settings: { + en: { + name: 'Curational', + description: + 'Subjective moderation geared towards curating a more positive environment.', + }, + }, + }, + }, + spam: { + id: 'spam', + configurable: true, + labels: [LABELS['spam']], + strings: { + settings: { + en: { + name: 'Spam', + description: "Content which doesn't add to the conversation.", + }, + }, + }, + }, + misinfo: { + id: 'misinfo', + configurable: true, + labels: [ + LABELS['account-security'], + LABELS['net-abuse'], + LABELS['impersonation'], + LABELS['scam'], + ], + strings: { + settings: { + en: { + name: 'Misinformation', + description: 'Content which misleads or defrauds users.', + }, + }, + }, + }, +} diff --git a/packages/api/src/moderation/const/labels.ts b/packages/api/src/moderation/const/labels.ts new file mode 100644 index 00000000000..da12706d442 --- /dev/null +++ b/packages/api/src/moderation/const/labels.ts @@ -0,0 +1,798 @@ +/** this doc is generated by ./scripts/code/labels.mjs **/ +import { LabelDefinitionMap } from '../types' + +export const LABELS: LabelDefinitionMap = { + '!hide': { + id: '!hide', + preferences: ['hide'], + flags: ['no-override'], + onwarn: 'blur', + groupId: 'system', + configurable: false, + strings: { + settings: { + en: { + name: 'Moderator Hide', + description: 'Moderator has chosen to hide the content.', + }, + }, + account: { + en: { + name: 'Content Blocked', + description: 'This account has been hidden by the moderators.', + }, + }, + content: { + en: { + name: 'Content Blocked', + description: 'This content has been hidden by the moderators.', + }, + }, + }, + }, + '!no-promote': { + id: '!no-promote', + preferences: ['hide'], + flags: [], + onwarn: null, + groupId: 'system', + configurable: false, + strings: { + settings: { + en: { + name: 'Moderator Filter', + description: 'Moderator has chosen to filter the content from feeds.', + }, + }, + account: { + en: { + name: 'N/A', + description: 'N/A', + }, + }, + content: { + en: { + name: 'N/A', + description: 'N/A', + }, + }, + }, + }, + '!warn': { + id: '!warn', + preferences: ['warn'], + flags: [], + onwarn: 'blur', + groupId: 'system', + configurable: false, + strings: { + settings: { + en: { + name: 'Moderator Warn', + description: + 'Moderator has chosen to set a general warning on the content.', + }, + }, + account: { + en: { + name: 'Content Warning', + description: + 'This account has received a general warning from moderators.', + }, + }, + content: { + en: { + name: 'Content Warning', + description: + 'This content has received a general warning from moderators.', + }, + }, + }, + }, + 'dmca-violation': { + id: 'dmca-violation', + preferences: ['hide'], + flags: ['no-override'], + onwarn: 'blur', + groupId: 'legal', + configurable: false, + strings: { + settings: { + en: { + name: 'Copyright Violation', + description: 'The content has received a DMCA takedown request.', + }, + }, + account: { + en: { + name: 'Copyright Violation', + description: + 'This account has received a DMCA takedown request. It will be restored if the concerns can be resolved.', + }, + }, + content: { + en: { + name: 'Copyright Violation', + description: + 'This content has received a DMCA takedown request. It will be restored if the concerns can be resolved.', + }, + }, + }, + }, + doxxing: { + id: 'doxxing', + preferences: ['hide'], + flags: ['no-override'], + onwarn: 'blur', + groupId: 'legal', + configurable: false, + strings: { + settings: { + en: { + name: 'Doxxing', + description: + 'Information that reveals private information about someone which has been shared without the consent of the subject.', + }, + }, + account: { + en: { + name: 'Doxxing', + description: + 'This account has been reported to publish private information about someone without their consent. This report is currently under review.', + }, + }, + content: { + en: { + name: 'Doxxing', + description: + 'This content has been reported to include private information about someone without their consent.', + }, + }, + }, + }, + porn: { + id: 'porn', + preferences: ['ignore', 'warn', 'hide'], + flags: ['adult'], + onwarn: 'blur-media', + groupId: 'sexual', + configurable: true, + strings: { + settings: { + en: { + name: 'Pornography', + description: + 'Images of full-frontal nudity (genitalia) in any sexualized context, or explicit sexual activity (meaning contact with genitalia or breasts) even if partially covered. Includes graphic sexual cartoons (often jokes/memes).', + }, + }, + account: { + en: { + name: 'Pornography', + description: + 'This account contains imagery of full-frontal nudity or explicit sexual activity.', + }, + }, + content: { + en: { + name: 'Pornography', + description: + 'This content contains imagery of full-frontal nudity or explicit sexual activity.', + }, + }, + }, + }, + sexual: { + id: 'sexual', + preferences: ['ignore', 'warn', 'hide'], + flags: ['adult'], + onwarn: 'blur-media', + groupId: 'sexual', + configurable: true, + strings: { + settings: { + en: { + name: 'Sexually Suggestive', + description: + 'Content that does not meet the level of "pornography", but is still sexual. Some common examples have been selfies and "hornyposting" with underwear on, or partially naked (naked but covered, eg with hands or from side perspective). Sheer/see-through nipples may end up in this category.', + }, + }, + account: { + en: { + name: 'Sexually Suggestive', + description: + 'This account contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress.', + }, + }, + content: { + en: { + name: 'Sexually Suggestive', + description: + 'This content contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress.', + }, + }, + }, + }, + nudity: { + id: 'nudity', + preferences: ['ignore', 'warn', 'hide'], + flags: ['adult'], + onwarn: 'blur-media', + groupId: 'sexual', + configurable: true, + strings: { + settings: { + en: { + name: 'Nudity', + description: + 'Nudity which is not sexual, or that is primarily "artistic" in nature. For example: breastfeeding; classic art paintings and sculptures; newspaper images with some nudity; fashion modeling. "Erotic photography" is likely to end up in sexual or porn.', + }, + }, + account: { + en: { + name: 'Nudity', + description: + 'This account contains imagery which portrays nudity in a non-sexual or artistic setting.', + }, + }, + content: { + en: { + name: 'Nudity', + description: + 'This content contains imagery which portrays nudity in a non-sexual or artistic setting.', + }, + }, + }, + }, + nsfl: { + id: 'nsfl', + preferences: ['ignore', 'warn', 'hide'], + flags: ['adult'], + onwarn: 'blur-media', + groupId: 'violence', + configurable: true, + strings: { + settings: { + en: { + name: 'NSFL', + description: + '"Not Suitable For Life." This includes graphic images like the infamous "goatse" (don\'t look it up).', + }, + }, + account: { + en: { + name: 'Graphic Imagery (NSFL)', + description: + 'This account contains graphic images which are often referred to as "Not Suitable For Life."', + }, + }, + content: { + en: { + name: 'Graphic Imagery (NSFL)', + description: + 'This content contains graphic images which are often referred to as "Not Suitable For Life."', + }, + }, + }, + }, + corpse: { + id: 'corpse', + preferences: ['ignore', 'warn', 'hide'], + flags: ['adult'], + onwarn: 'blur-media', + groupId: 'violence', + configurable: true, + strings: { + settings: { + en: { + name: 'Corpse', + description: + 'Visual image of a dead human body in any context. Includes war images, hanging, funeral caskets. Does not include all figurative cases (cartoons), but can include realistic figurative images or renderings.', + }, + }, + account: { + en: { + name: 'Graphic Imagery (Corpse)', + description: + 'This account contains images of a dead human body in any context. Includes war images, hanging, funeral caskets.', + }, + }, + content: { + en: { + name: 'Graphic Imagery (Corpse)', + description: + 'This content contains images of a dead human body in any context. Includes war images, hanging, funeral caskets.', + }, + }, + }, + }, + gore: { + id: 'gore', + preferences: ['ignore', 'warn', 'hide'], + flags: ['adult'], + onwarn: 'blur-media', + groupId: 'violence', + configurable: true, + strings: { + settings: { + en: { + name: 'Gore', + description: + 'Intended for shocking images, typically involving blood or visible wounds.', + }, + }, + account: { + en: { + name: 'Graphic Imagery (Gore)', + description: + 'This account contains shocking images involving blood or visible wounds.', + }, + }, + content: { + en: { + name: 'Graphic Imagery (Gore)', + description: + 'This content contains shocking images involving blood or visible wounds.', + }, + }, + }, + }, + torture: { + id: 'torture', + preferences: ['ignore', 'warn', 'hide'], + flags: ['adult'], + onwarn: 'blur', + groupId: 'violence', + configurable: true, + strings: { + settings: { + en: { + name: 'Torture', + description: + 'Depictions of torture of a human or animal (animal cruelty).', + }, + }, + account: { + en: { + name: 'Graphic Imagery (Torture)', + description: + 'This account contains depictions of torture of a human or animal.', + }, + }, + content: { + en: { + name: 'Graphic Imagery (Torture)', + description: + 'This content contains depictions of torture of a human or animal.', + }, + }, + }, + }, + 'self-harm': { + id: 'self-harm', + preferences: ['ignore', 'warn', 'hide'], + flags: ['adult'], + onwarn: 'blur-media', + groupId: 'violence', + configurable: true, + strings: { + settings: { + en: { + name: 'Self-Harm', + description: + 'A visual depiction (photo or figurative) of cutting, suicide, or similar.', + }, + }, + account: { + en: { + name: 'Graphic Imagery (Self-Harm)', + description: + 'This account includes depictions of cutting, suicide, or other forms of self-harm.', + }, + }, + content: { + en: { + name: 'Graphic Imagery (Self-Harm)', + description: + 'This content includes depictions of cutting, suicide, or other forms of self-harm.', + }, + }, + }, + }, + 'intolerant-race': { + id: 'intolerant-race', + preferences: ['ignore', 'warn', 'hide'], + flags: [], + onwarn: 'blur', + groupId: 'intolerance', + configurable: true, + strings: { + settings: { + en: { + name: 'Racial Intolerance', + description: 'Hateful or intolerant content related to race.', + }, + }, + account: { + en: { + name: 'Intolerance (Racial)', + description: + 'This account includes hateful or intolerant content related to race.', + }, + }, + content: { + en: { + name: 'Intolerance (Racial)', + description: + 'This content includes hateful or intolerant views related to race.', + }, + }, + }, + }, + 'intolerant-gender': { + id: 'intolerant-gender', + preferences: ['ignore', 'warn', 'hide'], + flags: [], + onwarn: 'blur', + groupId: 'intolerance', + configurable: true, + strings: { + settings: { + en: { + name: 'Gender Intolerance', + description: + 'Hateful or intolerant content related to gender or gender identity.', + }, + }, + account: { + en: { + name: 'Intolerance (Gender)', + description: + 'This account includes hateful or intolerant content related to gender or gender identity.', + }, + }, + content: { + en: { + name: 'Intolerance (Gender)', + description: + 'This content includes hateful or intolerant views related to gender or gender identity.', + }, + }, + }, + }, + 'intolerant-sexual-orientation': { + id: 'intolerant-sexual-orientation', + preferences: ['ignore', 'warn', 'hide'], + flags: [], + onwarn: 'blur', + groupId: 'intolerance', + configurable: true, + strings: { + settings: { + en: { + name: 'Sexual Orientation Intolerance', + description: + 'Hateful or intolerant content related to sexual preferences.', + }, + }, + account: { + en: { + name: 'Intolerance (Orientation)', + description: + 'This account includes hateful or intolerant content related to sexual preferences.', + }, + }, + content: { + en: { + name: 'Intolerance (Orientation)', + description: + 'This content includes hateful or intolerant views related to sexual preferences.', + }, + }, + }, + }, + 'intolerant-religion': { + id: 'intolerant-religion', + preferences: ['ignore', 'warn', 'hide'], + flags: [], + onwarn: 'blur', + groupId: 'intolerance', + configurable: true, + strings: { + settings: { + en: { + name: 'Religious Intolerance', + description: + 'Hateful or intolerant content related to religious views or practices.', + }, + }, + account: { + en: { + name: 'Intolerance (Religious)', + description: + 'This account includes hateful or intolerant content related to religious views or practices.', + }, + }, + content: { + en: { + name: 'Intolerance (Religious)', + description: + 'This content includes hateful or intolerant views related to religious views or practices.', + }, + }, + }, + }, + intolerant: { + id: 'intolerant', + preferences: ['ignore', 'warn', 'hide'], + flags: [], + onwarn: 'blur', + groupId: 'intolerance', + configurable: true, + strings: { + settings: { + en: { + name: 'Intolerance', + description: + 'A catchall for hateful or intolerant content which is not covered elsewhere.', + }, + }, + account: { + en: { + name: 'Intolerance', + description: 'This account includes hateful or intolerant content.', + }, + }, + content: { + en: { + name: 'Intolerance', + description: 'This content includes hateful or intolerant views.', + }, + }, + }, + }, + 'icon-intolerant': { + id: 'icon-intolerant', + preferences: ['ignore', 'warn', 'hide'], + flags: [], + onwarn: 'blur-media', + groupId: 'intolerance', + configurable: true, + strings: { + settings: { + en: { + name: 'Intolerant Iconography', + description: + 'Visual imagery associated with a hate group, such as the KKK or Nazi, in any context (supportive, critical, documentary, etc).', + }, + }, + account: { + en: { + name: 'Intolerant Iconography', + description: + 'This account includes imagery associated with a hate group such as the KKK or Nazis. This warning may apply to content any context, including critical or documentary purposes.', + }, + }, + content: { + en: { + name: 'Intolerant Iconography', + description: + 'This content includes imagery associated with a hate group such as the KKK or Nazis. This warning may apply to content any context, including critical or documentary purposes.', + }, + }, + }, + }, + threat: { + id: 'threat', + preferences: ['ignore', 'warn', 'hide'], + flags: [], + onwarn: 'blur', + groupId: 'rude', + configurable: true, + strings: { + settings: { + en: { + name: 'Threats', + description: + 'Statements or imagery published with the intent to threaten, intimidate, or harm.', + }, + }, + account: { + en: { + name: 'Threats', + description: + 'The moderators believe this account has published statements or imagery with the intent to threaten, intimidate, or harm others.', + }, + }, + content: { + en: { + name: 'Threats', + description: + 'The moderators believe this content was published with the intent to threaten, intimidate, or harm others.', + }, + }, + }, + }, + spoiler: { + id: 'spoiler', + preferences: ['ignore', 'warn', 'hide'], + flags: [], + onwarn: 'blur', + groupId: 'curation', + configurable: true, + strings: { + settings: { + en: { + name: 'Spoiler', + description: + 'Discussion about film, TV, etc which gives away plot points.', + }, + }, + account: { + en: { + name: 'Spoiler Warning', + description: + 'This account contains discussion about film, TV, etc which gives away plot points.', + }, + }, + content: { + en: { + name: 'Spoiler Warning', + description: + 'This content contains discussion about film, TV, etc which gives away plot points.', + }, + }, + }, + }, + spam: { + id: 'spam', + preferences: ['ignore', 'warn', 'hide'], + flags: [], + onwarn: 'blur', + groupId: 'spam', + configurable: true, + strings: { + settings: { + en: { + name: 'Spam', + description: + 'Repeat, low-quality messages which are clearly not designed to add to a conversation or space.', + }, + }, + account: { + en: { + name: 'Spam', + description: + 'This account publishes repeat, low-quality messages which are clearly not designed to add to a conversation or space.', + }, + }, + content: { + en: { + name: 'Spam', + description: + 'This content is a part of repeat, low-quality messages which are clearly not designed to add to a conversation or space.', + }, + }, + }, + }, + 'account-security': { + id: 'account-security', + preferences: ['ignore', 'warn', 'hide'], + flags: [], + onwarn: 'blur', + groupId: 'misinfo', + configurable: true, + strings: { + settings: { + en: { + name: 'Security Concerns', + description: + 'Content designed to hijack user accounts such as a phishing attack.', + }, + }, + account: { + en: { + name: 'Security Warning', + description: + 'This account has published content designed to hijack user accounts such as a phishing attack.', + }, + }, + content: { + en: { + name: 'Security Warning', + description: + 'This content is designed to hijack user accounts such as a phishing attack.', + }, + }, + }, + }, + 'net-abuse': { + id: 'net-abuse', + preferences: ['ignore', 'warn', 'hide'], + flags: [], + onwarn: 'blur', + groupId: 'misinfo', + configurable: true, + strings: { + settings: { + en: { + name: 'Network Attacks', + description: + 'Content designed to attack network systems such as denial-of-service attacks.', + }, + }, + account: { + en: { + name: 'Network Attack Warning', + description: + 'This account has published content designed to attack network systems such as denial-of-service attacks.', + }, + }, + content: { + en: { + name: 'Network Attack Warning', + description: + 'This content is designed to attack network systems such as denial-of-service attacks.', + }, + }, + }, + }, + impersonation: { + id: 'impersonation', + preferences: ['ignore', 'warn', 'hide'], + flags: [], + onwarn: 'alert', + groupId: 'misinfo', + configurable: true, + strings: { + settings: { + en: { + name: 'Impersonation', + description: 'Accounts which falsely assert some identity.', + }, + }, + account: { + en: { + name: 'Impersonation Warning', + description: + 'The moderators believe this account is lying about their identity.', + }, + }, + content: { + en: { + name: 'Impersonation Warning', + description: + 'The moderators believe this account is lying about their identity.', + }, + }, + }, + }, + scam: { + id: 'scam', + preferences: ['ignore', 'warn', 'hide'], + flags: [], + onwarn: 'alert', + groupId: 'misinfo', + configurable: true, + strings: { + settings: { + en: { + name: 'Scam', + description: 'Fraudulent content.', + }, + }, + account: { + en: { + name: 'Scam Warning', + description: + 'The moderators believe this account publishes fraudulent content.', + }, + }, + content: { + en: { + name: 'Scam Warning', + description: 'The moderators believe this is fraudulent content.', + }, + }, + }, + }, +} diff --git a/packages/api/src/moderation/index.ts b/packages/api/src/moderation/index.ts new file mode 100644 index 00000000000..04edfc3e600 --- /dev/null +++ b/packages/api/src/moderation/index.ts @@ -0,0 +1,343 @@ +import { AppBskyActorDefs } from '../client/index' +import { + ModerationSubjectProfile, + ModerationSubjectPost, + ModerationSubjectFeedGenerator, + ModerationSubjectUserList, + ModerationOpts, + ModerationDecision, + ModerationUI, +} from './types' +import { decideAccount } from './subjects/account' +import { decideProfile } from './subjects/profile' +import { decidePost } from './subjects/post' +import { + decideQuotedPost, + decideQuotedPostAccount, + decideQuotedPostWithMedia, + decideQuotedPostWithMediaAccount, +} from './subjects/quoted-post' +import { decideFeedGenerator } from './subjects/feed-generator' +import { decideUserList } from './subjects/user-list' +import { + takeHighestPriorityDecision, + downgradeDecision, + isModerationDecisionNoop, + isQuotedPost, + isQuotedPostWithMedia, + toModerationUI, +} from './util' + +// profiles +// = + +export interface ProfileModeration { + decisions: { + account: ModerationDecision + profile: ModerationDecision + } + account: ModerationUI + profile: ModerationUI + avatar: ModerationUI +} + +export function moderateProfile( + subject: ModerationSubjectProfile, + opts: ModerationOpts, +): ProfileModeration { + // decide the moderation the account and the profile + const account = decideAccount(subject, opts) + const profile = decideProfile(subject, opts) + + // if the decision is supposed to blur media, + // - have it apply to the view if it's on the account + // - otherwise ignore it + if (account.blurMedia) { + account.blur = true + } + + // dont give profile.filter because that is meaningless + profile.filter = false + + // downgrade based on authorship + if (!isModerationDecisionNoop(account) && account.did === opts.userDid) { + downgradeDecision(account, { alert: true }) + } + if (!isModerationDecisionNoop(profile) && profile.did === opts.userDid) { + downgradeDecision(profile, { alert: true }) + } + + // derive avatar blurring from account & profile, but override for mutes because that shouldnt blur + let avatarBlur = false + let avatarNoOverride = false + if ((account.blur || account.blurMedia) && account.cause?.type !== 'muted') { + avatarBlur = true + avatarNoOverride = account.noOverride || profile.noOverride + } else if (profile.blur || profile.blurMedia) { + avatarBlur = true + avatarNoOverride = account.noOverride || profile.noOverride + } + + // dont blur the account for blocking & muting + if ( + account.cause?.type === 'blocking' || + account.cause?.type === 'blocked-by' || + account.cause?.type === 'muted' + ) { + account.blur = false + account.noOverride = false + } + + return { + decisions: { account, profile }, + + // moderate all content based on account + account: + account.filter || account.blur || account.alert + ? toModerationUI(account) + : {}, + + // moderate the profile details based on the profile + profile: + profile.filter || profile.blur || profile.alert + ? toModerationUI(profile) + : {}, + + // blur or alert the avatar based on the account and profile decisions + avatar: { + blur: avatarBlur, + alert: account.alert || profile.alert, + noOverride: avatarNoOverride, + }, + } +} + +// posts +// = + +export interface PostModeration { + decisions: { + post: ModerationDecision + account: ModerationDecision + profile: ModerationDecision + quote?: ModerationDecision + quotedAccount?: ModerationDecision + } + content: ModerationUI + avatar: ModerationUI + embed: ModerationUI +} + +export function moderatePost( + subject: ModerationSubjectPost, + opts: ModerationOpts, +): PostModeration { + // decide the moderation for the post, the post author's account, + // and the post author's profile + const post = decidePost(subject, opts) + const account = decideAccount(subject.author, opts) + const profile = decideProfile(subject.author, opts) + + // decide the moderation for any quoted posts + let quote: ModerationDecision | undefined + let quotedAccount: ModerationDecision | undefined + if (isQuotedPost(subject.embed)) { + quote = decideQuotedPost(subject.embed, opts) + quotedAccount = decideQuotedPostAccount(subject.embed, opts) + } else if (isQuotedPostWithMedia(subject.embed)) { + quote = decideQuotedPostWithMedia(subject.embed, opts) + quotedAccount = decideQuotedPostWithMediaAccount(subject.embed, opts) + } + + // downgrade based on authorship + if (!isModerationDecisionNoop(post) && post.did === opts.userDid) { + downgradeDecision(post, { alert: true }) + } + if (!isModerationDecisionNoop(account) && account.did === opts.userDid) { + downgradeDecision(account, { alert: false }) + } + if (!isModerationDecisionNoop(profile) && profile.did === opts.userDid) { + downgradeDecision(profile, { alert: false }) + } + if (quote && !isModerationDecisionNoop(quote) && quote.did === opts.userDid) { + downgradeDecision(quote, { alert: true }) + } + if ( + quotedAccount && + !isModerationDecisionNoop(quotedAccount) && + quotedAccount.did === opts.userDid + ) { + downgradeDecision(quotedAccount, { alert: false }) + } + + // derive filtering from feeds from the post, post author's account, + // quoted post, and quoted post author's account + const mergedForFeed = takeHighestPriorityDecision( + post, + account, + quote, + quotedAccount, + ) + + // derive view blurring from the post and the post author's account + const mergedForView = takeHighestPriorityDecision(post, account) + + // derive embed blurring from the quoted post and the quoted post author's account + const mergedQuote = takeHighestPriorityDecision(quote, quotedAccount) + + // derive avatar blurring from account & profile, but override for mutes because that shouldnt blur + let blurAvatar = false + if ((account.blur || account.blurMedia) && account.cause?.type !== 'muted') { + blurAvatar = true + } else if ( + (profile.blur || profile.blurMedia) && + profile.cause?.type !== 'muted' + ) { + blurAvatar = true + } + + return { + decisions: { post, account, profile, quote, quotedAccount }, + + // content behaviors are pulled from feed and view derivations above + content: { + cause: !isModerationDecisionNoop(mergedForView) + ? mergedForView.cause + : mergedForFeed.filter + ? mergedForFeed.cause + : undefined, + filter: mergedForFeed.filter, + blur: mergedForView.blur, + alert: mergedForView.alert, + noOverride: mergedForView.noOverride, + }, + + // blur or alert the avatar based on the account and profile decisions + avatar: { + blur: blurAvatar, + alert: account.alert || profile.alert, + noOverride: account.noOverride || profile.noOverride, + }, + + // blur the embed if the quoted post required it, + // or else if the account or post decision was to blur media + embed: !isModerationDecisionNoop(mergedQuote, { ignoreFilter: true }) + ? { + cause: mergedQuote.cause, + blur: mergedQuote.blur, + alert: mergedQuote.alert, + noOverride: mergedQuote.noOverride, + } + : account.blurMedia + ? { + cause: account.cause, + blur: true, + noOverride: account.noOverride, + } + : post.blurMedia + ? { + cause: post.cause, + blur: true, + noOverride: post.noOverride, + } + : {}, + } +} + +// feed generators +// = + +export interface FeedGeneratorModeration { + decisions: { + feedGenerator: ModerationDecision + account: ModerationDecision + profile: ModerationDecision + } + content: ModerationUI + avatar: ModerationUI +} + +export function moderateFeedGenerator( + subject: ModerationSubjectFeedGenerator, + opts: ModerationOpts, +): FeedGeneratorModeration { + // decide the moderation for the generator, the generator creator's account, + // and the generator creator's profile + const feedGenerator = decideFeedGenerator(subject, opts) + const account = decideAccount(subject.creator, opts) + const profile = decideProfile(subject.creator, opts) + + // derive behaviors from feeds from the generator and the generator's account + const merged = takeHighestPriorityDecision(feedGenerator, account) + + return { + decisions: { feedGenerator, account, profile }, + + // content behaviors are pulled from merged decisions + content: { + cause: isModerationDecisionNoop(merged) ? undefined : merged.cause, + filter: merged.filter, + blur: merged.blur, + alert: merged.alert, + noOverride: merged.noOverride, + }, + + // blur or alert the avatar based on the account and profile decisions + avatar: { + blur: account.blurMedia || profile.blurMedia, + alert: account.alert, + noOverride: account.noOverride || profile.noOverride, + }, + } +} + +// user lists +// = + +export interface UserListModeration { + decisions: { + userList: ModerationDecision + account: ModerationDecision + profile: ModerationDecision + } + content: ModerationUI + avatar: ModerationUI +} + +export function moderateUserList( + subject: ModerationSubjectUserList, + opts: ModerationOpts, +): UserListModeration { + // decide the moderation for the list, the list creator's account, + // and the list creator's profile + const userList = decideUserList(subject, opts) + const account = AppBskyActorDefs.isProfileViewBasic(subject.creator) + ? decideAccount(subject.creator, opts) + : ModerationDecision.noop() + const profile = AppBskyActorDefs.isProfileViewBasic(subject.creator) + ? decideProfile(subject.creator, opts) + : ModerationDecision.noop() + + // derive behaviors from feeds from the list and the list's account + const merged = takeHighestPriorityDecision(userList, account) + + return { + decisions: { userList, account, profile }, + + // content behaviors are pulled from merged decisions + content: { + cause: isModerationDecisionNoop(merged) ? undefined : merged.cause, + filter: merged.filter, + blur: merged.blur, + alert: merged.alert, + noOverride: merged.noOverride, + }, + + // blur or alert the avatar based on the account and profile decisions + avatar: { + blur: account.blurMedia || profile.blurMedia, + alert: account.alert, + noOverride: account.noOverride || profile.noOverride, + }, + } +} diff --git a/packages/api/src/moderation/subjects/account.ts b/packages/api/src/moderation/subjects/account.ts new file mode 100644 index 00000000000..5b6d74369e2 --- /dev/null +++ b/packages/api/src/moderation/subjects/account.ts @@ -0,0 +1,40 @@ +import { ModerationCauseAccumulator } from '../accumulator' +import { + Label, + ModerationSubjectProfile, + ModerationOpts, + ModerationDecision, +} from '../types' + +export function decideAccount( + subject: ModerationSubjectProfile, + opts: ModerationOpts, +): ModerationDecision { + const acc = new ModerationCauseAccumulator() + + acc.setDid(subject.did) + if (subject.viewer?.muted) { + if (subject.viewer?.mutedByList) { + acc.addMutedByList(subject.viewer?.mutedByList) + } else { + acc.addMuted(subject.viewer?.muted) + } + } + acc.addBlocking(subject.viewer?.blocking) + acc.addBlockedBy(subject.viewer?.blockedBy) + + for (const label of filterAccountLabels(subject.labels)) { + acc.addLabel(label, opts) + } + + return acc.finalizeDecision(opts) +} + +export function filterAccountLabels(labels?: Label[]): Label[] { + if (!labels) { + return [] + } + return labels.filter( + (label) => !label.uri.endsWith('/app.bsky.actor.profile/self'), + ) +} diff --git a/packages/api/src/moderation/subjects/feed-generator.ts b/packages/api/src/moderation/subjects/feed-generator.ts new file mode 100644 index 00000000000..1b75d502810 --- /dev/null +++ b/packages/api/src/moderation/subjects/feed-generator.ts @@ -0,0 +1,13 @@ +import { + ModerationSubjectFeedGenerator, + ModerationDecision, + ModerationOpts, +} from '../types' + +export function decideFeedGenerator( + subject: ModerationSubjectFeedGenerator, + opts: ModerationOpts, +): ModerationDecision { + // TODO handle labels applied on the feed generator itself + return ModerationDecision.noop() +} diff --git a/packages/api/src/moderation/subjects/post.ts b/packages/api/src/moderation/subjects/post.ts new file mode 100644 index 00000000000..577b5374df1 --- /dev/null +++ b/packages/api/src/moderation/subjects/post.ts @@ -0,0 +1,23 @@ +import { ModerationCauseAccumulator } from '../accumulator' +import { + ModerationSubjectPost, + ModerationOpts, + ModerationDecision, +} from '../types' + +export function decidePost( + subject: ModerationSubjectPost, + opts: ModerationOpts, +): ModerationDecision { + const acc = new ModerationCauseAccumulator() + + acc.setDid(subject.author.did) + + if (subject.labels?.length) { + for (const label of subject.labels) { + acc.addLabel(label, opts) + } + } + + return acc.finalizeDecision(opts) +} diff --git a/packages/api/src/moderation/subjects/profile.ts b/packages/api/src/moderation/subjects/profile.ts new file mode 100644 index 00000000000..0025bf690e5 --- /dev/null +++ b/packages/api/src/moderation/subjects/profile.ts @@ -0,0 +1,31 @@ +import { ModerationCauseAccumulator } from '../accumulator' +import { + Label, + ModerationSubjectProfile, + ModerationOpts, + ModerationDecision, +} from '../types' + +export function decideProfile( + subject: ModerationSubjectProfile, + opts: ModerationOpts, +): ModerationDecision { + const acc = new ModerationCauseAccumulator() + + acc.setDid(subject.did) + + for (const label of filterProfileLabels(subject.labels)) { + acc.addLabel(label, opts) + } + + return acc.finalizeDecision(opts) +} + +export function filterProfileLabels(labels?: Label[]): Label[] { + if (!labels) { + return [] + } + return labels.filter((label) => + label.uri.endsWith('/app.bsky.actor.profile/self'), + ) +} diff --git a/packages/api/src/moderation/subjects/quoted-post.ts b/packages/api/src/moderation/subjects/quoted-post.ts new file mode 100644 index 00000000000..8aac01626a3 --- /dev/null +++ b/packages/api/src/moderation/subjects/quoted-post.ts @@ -0,0 +1,62 @@ +import { AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia } from '../../client' +import { ModerationCauseAccumulator } from '../accumulator' +import { ModerationOpts, ModerationDecision } from '../types' +import { decideAccount } from './account' + +export function decideQuotedPost( + subject: AppBskyEmbedRecord.View, + opts: ModerationOpts, +): ModerationDecision { + const acc = new ModerationCauseAccumulator() + + if (AppBskyEmbedRecord.isViewRecord(subject.record)) { + acc.setDid(subject.record.author.did) + + if (subject.record.labels?.length) { + for (const label of subject.record.labels) { + acc.addLabel(label, opts) + } + } + } + + return acc.finalizeDecision(opts) +} + +export function decideQuotedPostAccount( + subject: AppBskyEmbedRecord.View, + opts: ModerationOpts, +): ModerationDecision { + if (AppBskyEmbedRecord.isViewRecord(subject.record)) { + return decideAccount(subject.record.author, opts) + } + return ModerationDecision.noop() +} + +export function decideQuotedPostWithMedia( + subject: AppBskyEmbedRecordWithMedia.View, + opts: ModerationOpts, +): ModerationDecision { + const acc = new ModerationCauseAccumulator() + + if (AppBskyEmbedRecord.isViewRecord(subject.record.record)) { + acc.setDid(subject.record.record.author.did) + + if (subject.record.record.labels?.length) { + for (const label of subject.record.record.labels) { + acc.addLabel(label, opts) + } + } + } + + return acc.finalizeDecision(opts) +} + +export function decideQuotedPostWithMediaAccount( + subject: AppBskyEmbedRecordWithMedia.View, + opts: ModerationOpts, +): ModerationDecision { + if (AppBskyEmbedRecord.isViewRecord(subject.record.record)) { + return decideAccount(subject.record.record.author, opts) + } + return ModerationDecision.noop() +} diff --git a/packages/api/src/moderation/subjects/user-list.ts b/packages/api/src/moderation/subjects/user-list.ts new file mode 100644 index 00000000000..20c48ae523f --- /dev/null +++ b/packages/api/src/moderation/subjects/user-list.ts @@ -0,0 +1,13 @@ +import { + ModerationSubjectUserList, + ModerationOpts, + ModerationDecision, +} from '../types' + +export function decideUserList( + subject: ModerationSubjectUserList, + opts: ModerationOpts, +): ModerationDecision { + // TODO handle labels applied on the list itself + return ModerationDecision.noop() +} diff --git a/packages/api/src/moderation/types.ts b/packages/api/src/moderation/types.ts new file mode 100644 index 00000000000..feeeed7708f --- /dev/null +++ b/packages/api/src/moderation/types.ts @@ -0,0 +1,141 @@ +import { + AppBskyActorDefs, + AppBskyFeedDefs, + AppBskyGraphDefs, + ComAtprotoLabelDefs, +} from '../client/index' + +// labels +// = + +export type Label = ComAtprotoLabelDefs.Label + +export type LabelPreference = 'ignore' | 'warn' | 'hide' +export type LabelDefinitionFlag = 'no-override' | 'adult' +export type LabelDefinitionOnWarnBehavior = + | 'blur' + | 'blur-media' + | 'alert' + | null + +export interface LabelDefinitionLocalizedStrings { + name: string + description: string +} + +export type LabelDefinitionLocalizedStringsMap = Record< + string, + LabelDefinitionLocalizedStrings +> + +export interface LabelDefinition { + id: string + groupId: string + configurable: boolean + preferences: LabelPreference[] + flags: LabelDefinitionFlag[] + onwarn: LabelDefinitionOnWarnBehavior + strings: { + settings: LabelDefinitionLocalizedStringsMap + account: LabelDefinitionLocalizedStringsMap + content: LabelDefinitionLocalizedStringsMap + } +} + +export interface LabelGroupDefinition { + id: string + configurable: boolean + labels: LabelDefinition[] + strings: { + settings: LabelDefinitionLocalizedStringsMap + } +} + +export type LabelDefinitionMap = Record +export type LabelGroupDefinitionMap = Record + +// labelers +// = + +interface Labeler { + did: string + displayName: string +} + +export interface LabelerSettings { + labeler: Labeler + settings: Record +} + +// subjects +// = + +export type ModerationSubjectProfile = + | AppBskyActorDefs.ProfileViewBasic + | AppBskyActorDefs.ProfileView + | AppBskyActorDefs.ProfileViewDetailed + +export type ModerationSubjectPost = AppBskyFeedDefs.PostView + +export type ModerationSubjectFeedGenerator = AppBskyFeedDefs.GeneratorView + +export type ModerationSubjectUserList = + | AppBskyGraphDefs.ListViewBasic + | AppBskyGraphDefs.ListView + +export type ModerationSubject = + | ModerationSubjectProfile + | ModerationSubjectPost + | ModerationSubjectFeedGenerator + | ModerationSubjectUserList + +// behaviors +// = + +export type ModerationCauseSource = + | { type: 'user' } + | { type: 'list'; list: AppBskyGraphDefs.ListViewBasic } + +export type ModerationCause = + | { type: 'blocking'; source: ModerationCauseSource; priority: 3 } + | { type: 'blocked-by'; source: ModerationCauseSource; priority: 4 } + | { + type: 'label' + labeler: Labeler + label: Label + labelDef: LabelDefinition + setting: LabelPreference + priority: 1 | 2 | 5 | 7 | 8 + } + | { type: 'muted'; source: ModerationCauseSource; priority: 6 } + +export interface ModerationOpts { + userDid: string + adultContentEnabled: boolean + labelerSettings: LabelerSettings[] +} + +export class ModerationDecision { + static noop() { + return new ModerationDecision() + } + + constructor( + public cause: ModerationCause | undefined = undefined, + public alert: boolean = false, + public blur: boolean = false, + public blurMedia: boolean = false, + public filter: boolean = false, + public noOverride: boolean = false, + public additionalCauses: ModerationCause[] = [], + public did: string = '', + ) {} +} + +export interface ModerationUI { + filter?: boolean + blur?: boolean + alert?: boolean + cause?: ModerationCause + noOverride?: boolean +} diff --git a/packages/api/src/moderation/util.ts b/packages/api/src/moderation/util.ts new file mode 100644 index 00000000000..56330c2a458 --- /dev/null +++ b/packages/api/src/moderation/util.ts @@ -0,0 +1,98 @@ +import { + AppBskyEmbedRecord, + AppBskyEmbedRecordWithMedia, + AppBskyFeedPost, +} from '../client' +import { ModerationDecision, ModerationUI } from './types' + +export function takeHighestPriorityDecision( + ...decisions: (ModerationDecision | undefined)[] +): ModerationDecision { + // remove undefined decisions + const filtered = decisions.filter((d) => !!d) as ModerationDecision[] + if (filtered.length === 0) { + return ModerationDecision.noop() + } + + // sort by highest priority + filtered.sort((a, b) => { + if (a.cause && b.cause) { + return a.cause.priority - b.cause.priority + } + if (a.cause) { + return -1 + } + if (b.cause) { + return 1 + } + return 0 + }) + + // use the top priority + return filtered[0] +} + +export function downgradeDecision( + decision: ModerationDecision, + { alert }: { alert: boolean }, +) { + decision.blur = false + decision.blurMedia = false + decision.filter = false + decision.noOverride = false + decision.alert = alert + if (!alert) { + delete decision.cause + } +} + +export function isModerationDecisionNoop( + decision: ModerationDecision | undefined, + { ignoreFilter }: { ignoreFilter: boolean } = { ignoreFilter: false }, +): boolean { + if (!decision) { + return true + } + if (decision.alert) { + return false + } + if (decision.blur) { + return false + } + if (decision.filter && !ignoreFilter) { + return false + } + return true +} + +export function isQuotedPost(embed: unknown): embed is AppBskyEmbedRecord.View { + return Boolean( + embed && + AppBskyEmbedRecord.isView(embed) && + AppBskyEmbedRecord.isViewRecord(embed.record) && + AppBskyFeedPost.isRecord(embed.record.value) && + AppBskyFeedPost.validateRecord(embed.record.value).success, + ) +} + +export function isQuotedPostWithMedia( + embed: unknown, +): embed is AppBskyEmbedRecordWithMedia.View { + return Boolean( + embed && + AppBskyEmbedRecordWithMedia.isView(embed) && + AppBskyEmbedRecord.isViewRecord(embed.record.record) && + AppBskyFeedPost.isRecord(embed.record.record.value) && + AppBskyFeedPost.validateRecord(embed.record.record.value).success, + ) +} + +export function toModerationUI(decision: ModerationDecision): ModerationUI { + return { + cause: decision.cause, + filter: decision.filter, + blur: decision.blur, + alert: decision.alert, + noOverride: decision.noOverride, + } +} diff --git a/packages/api/tests/_util.ts b/packages/api/tests/_util.ts deleted file mode 100644 index e7848d50ea4..00000000000 --- a/packages/api/tests/_util.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { AtpAgentFetchHandlerResponse } from '..' - -export async function fetchHandler( - httpUri: string, - httpMethod: string, - httpHeaders: Record, - httpReqBody: unknown, -): Promise { - // The duplex field is now required for streaming bodies, but not yet reflected - // anywhere in docs or types. See whatwg/fetch#1438, nodejs/node#46221. - const reqInit: RequestInit & { duplex: string } = { - method: httpMethod, - headers: httpHeaders, - body: httpReqBody - ? new TextEncoder().encode(JSON.stringify(httpReqBody)) - : undefined, - duplex: 'half', - } - const res = await fetch(httpUri, reqInit) - const resBody = await res.arrayBuffer() - return { - status: res.status, - headers: Object.fromEntries(res.headers.entries()), - body: resBody ? JSON.parse(new TextDecoder().decode(resBody)) : undefined, - } -} diff --git a/packages/api/tests/post-moderation.test.ts b/packages/api/tests/post-moderation.test.ts new file mode 100644 index 00000000000..3d62a720507 --- /dev/null +++ b/packages/api/tests/post-moderation.test.ts @@ -0,0 +1,46 @@ +import { moderatePost } from '../src' +import type { + ModerationBehaviors, + ModerationBehaviorScenario, +} from '../definitions/moderation-behaviors' +import { ModerationBehaviorSuiteRunner } from './util/moderation-behavior' +import { readFileSync } from 'fs' +import { join } from 'path' + +const suite: ModerationBehaviors = JSON.parse( + readFileSync( + join(__dirname, '..', 'definitions', 'post-moderation-behaviors.json'), + 'utf8', + ), +) + +const suiteRunner = new ModerationBehaviorSuiteRunner(suite) + +describe('Post moderation behaviors', () => { + const scenarios = Array.from(Object.entries(suite.scenarios)) + it.each(scenarios)( + '%s', + (_name: string, scenario: ModerationBehaviorScenario) => { + const res = moderatePost( + suiteRunner.postScenario(scenario), + suiteRunner.moderationOpts(scenario), + ) + expect(res.content).toBeModerationResult( + scenario.behaviors.content, + 'post content', + JSON.stringify(res, null, 2), + ) + expect(res.avatar).toBeModerationResult( + scenario.behaviors.avatar, + 'post avatar', + JSON.stringify(res, null, 2), + true, + ) + expect(res.embed).toBeModerationResult( + scenario.behaviors.embed, + 'post embed', + JSON.stringify(res, null, 2), + ) + }, + ) +}) diff --git a/packages/api/tests/profile-moderation.test.ts b/packages/api/tests/profile-moderation.test.ts new file mode 100644 index 00000000000..bca63857c30 --- /dev/null +++ b/packages/api/tests/profile-moderation.test.ts @@ -0,0 +1,46 @@ +import { moderateProfile } from '../src' +import type { + ModerationBehaviors, + ModerationBehaviorScenario, +} from '../definitions/moderation-behaviors' +import { ModerationBehaviorSuiteRunner } from './util/moderation-behavior' +import { readFileSync } from 'fs' +import { join } from 'path' + +const suite: ModerationBehaviors = JSON.parse( + readFileSync( + join(__dirname, '..', 'definitions', 'profile-moderation-behaviors.json'), + 'utf8', + ), +) + +const suiteRunner = new ModerationBehaviorSuiteRunner(suite) + +describe('Post moderation behaviors', () => { + const scenarios = Array.from(Object.entries(suite.scenarios)) + it.each(scenarios)( + '%s', + (_name: string, scenario: ModerationBehaviorScenario) => { + const res = moderateProfile( + suiteRunner.profileScenario(scenario), + suiteRunner.moderationOpts(scenario), + ) + expect(res.account).toBeModerationResult( + scenario.behaviors.account, + 'account', + JSON.stringify(res, null, 2), + ) + expect(res.profile).toBeModerationResult( + scenario.behaviors.profile, + 'profile content', + JSON.stringify(res, null, 2), + ) + expect(res.avatar).toBeModerationResult( + scenario.behaviors.avatar, + 'profile avatar', + JSON.stringify(res, null, 2), + true, + ) + }, + ) +}) diff --git a/packages/api/tests/util/index.ts b/packages/api/tests/util/index.ts new file mode 100644 index 00000000000..d9cc5e90780 --- /dev/null +++ b/packages/api/tests/util/index.ts @@ -0,0 +1,176 @@ +import { + AtpAgentFetchHandlerResponse, + ComAtprotoLabelDefs, + AppBskyFeedDefs, + AppBskyActorDefs, + AppBskyFeedPost, + AppBskyEmbedRecord, + AppBskyGraphDefs, +} from '../../src' + +export async function fetchHandler( + httpUri: string, + httpMethod: string, + httpHeaders: Record, + httpReqBody: unknown, +): Promise { + // The duplex field is now required for streaming bodies, but not yet reflected + // anywhere in docs or types. See whatwg/fetch#1438, nodejs/node#46221. + const reqInit: RequestInit & { duplex: string } = { + method: httpMethod, + headers: httpHeaders, + body: httpReqBody + ? new TextEncoder().encode(JSON.stringify(httpReqBody)) + : undefined, + duplex: 'half', + } + const res = await fetch(httpUri, reqInit) + const resBody = await res.arrayBuffer() + return { + status: res.status, + headers: Object.fromEntries(res.headers.entries()), + body: resBody ? JSON.parse(new TextDecoder().decode(resBody)) : undefined, + } +} + +export const mock = { + post({ + text, + reply, + embed, + }: { + text: string + reply?: AppBskyFeedPost.ReplyRef + embed?: AppBskyFeedPost.Record['embed'] + }): AppBskyFeedPost.Record { + return { + $type: 'app.bsky.feed.post', + text, + reply, + embed, + langs: ['en'], + createdAt: new Date().toISOString(), + } + }, + + postView({ + record, + author, + embed, + replyCount, + repostCount, + likeCount, + viewer, + labels, + }: { + record: AppBskyFeedPost.Record + author: AppBskyActorDefs.ProfileViewBasic + embed?: AppBskyFeedDefs.PostView['embed'] + replyCount?: number + repostCount?: number + likeCount?: number + viewer?: AppBskyFeedDefs.ViewerState + labels?: ComAtprotoLabelDefs.Label[] + }): AppBskyFeedDefs.PostView { + return { + uri: `at://${author.did}/app.bsky.post/fake`, + cid: 'fake', + author, + record, + embed, + replyCount, + repostCount, + likeCount, + indexedAt: new Date().toISOString(), + viewer, + labels, + } + }, + + embedRecordView({ + record, + author, + labels, + }: { + record: AppBskyFeedPost.Record + author: AppBskyActorDefs.ProfileViewBasic + labels?: ComAtprotoLabelDefs.Label[] + }): AppBskyEmbedRecord.View { + return { + $type: 'app.bsky.embed.record#view', + record: { + $type: 'app.bsky.embed.record#viewRecord', + uri: `at://${author.did}/app.bsky.post/fake`, + cid: 'fake', + author, + value: record, + labels, + indexedAt: new Date().toISOString(), + }, + } + }, + + profileViewBasic({ + handle, + displayName, + viewer, + labels, + }: { + handle: string + displayName?: string + viewer?: AppBskyActorDefs.ViewerState + labels?: ComAtprotoLabelDefs.Label[] + }): AppBskyActorDefs.ProfileViewBasic { + return { + did: `did:web:${handle}`, + handle, + displayName, + viewer, + labels, + } + }, + + actorViewerState({ + muted, + mutedByList, + blockedBy, + blocking, + following, + followedBy, + }: { + muted?: boolean + mutedByList?: AppBskyGraphDefs.ListViewBasic + blockedBy?: boolean + blocking?: string + following?: string + followedBy?: string + }): AppBskyActorDefs.ViewerState { + return { + muted, + mutedByList, + blockedBy, + blocking, + following, + followedBy, + } + }, + + listViewBasic({ name }: { name: string }): AppBskyGraphDefs.ListViewBasic { + return { + uri: 'at://did:plc:fake/app.bsky.graph.list/fake', + cid: 'fake', + name, + purpose: 'app.bsky.graph.defs#modlist', + indexedAt: new Date().toISOString(), + } + }, + + label({ val, uri }: { val: string; uri: string }): ComAtprotoLabelDefs.Label { + return { + src: 'did:plc:fake-labeler', + uri, + val, + cts: new Date().toISOString(), + } + }, +} diff --git a/packages/api/tests/util/moderation-behavior.ts b/packages/api/tests/util/moderation-behavior.ts new file mode 100644 index 00000000000..0d78e382c6c --- /dev/null +++ b/packages/api/tests/util/moderation-behavior.ts @@ -0,0 +1,180 @@ +import { ModerationUI, ModerationOpts, ComAtprotoLabelDefs } from '../../src' +import type { + ModerationBehaviors, + ModerationBehaviorScenario, + ModerationBehaviorResult, +} from '../../definitions/moderation-behaviors' +import { mock as m } from './index' + +expect.extend({ + toBeModerationResult( + actual: ModerationUI, + expected: ModerationBehaviorResult | undefined, + context: string, + stringifiedResult: string, + ignoreCause = false, + ) { + const fail = (msg: string) => ({ + pass: false, + message: () => `${msg}. Full result: ${stringifiedResult}`, + }) + let cause = actual.cause?.type as string + if (actual.cause?.type === 'label') { + cause = `label:${actual.cause.labelDef.id}` + } else if (actual.cause?.type === 'muted') { + if (actual.cause.source.type === 'list') { + cause = 'muted-by-list' + } + } + if (!expected) { + if (!ignoreCause && actual.cause) { + return fail(`${context} expected to be a no-op, got ${cause}`) + } + if (actual.alert) { + return fail(`${context} expected to be a no-op, got alert=true`) + } + if (actual.blur) { + return fail(`${context} expected to be a no-op, got blur=true`) + } + if (actual.filter) { + return fail(`${context} expected to be a no-op, got filter=true`) + } + if (actual.noOverride) { + return fail(`${context} expected to be a no-op, got noOverride=true`) + } + } else { + if (!ignoreCause && cause !== expected.cause) { + return fail(`${context} expected to be ${expected.cause}, got ${cause}`) + } + if (!!actual.alert !== !!expected.alert) { + return fail( + `${context} expected to be alert=${expected.alert || false}, got ${ + actual.alert || false + }`, + ) + } + if (!!actual.blur !== !!expected.blur) { + return fail( + `${context} expected to be blur=${expected.blur || false}, got ${ + actual.blur || false + }`, + ) + } + if (!!actual.filter !== !!expected.filter) { + return fail( + `${context} expected to be filter=${expected.filter || false}, got ${ + actual.filter || false + }`, + ) + } + if (!!actual.noOverride !== !!expected.noOverride) { + return fail( + `${context} expected to be noOverride=${ + expected.noOverride || false + }, got ${actual.noOverride || false}`, + ) + } + } + return { pass: true, message: () => '' } + }, +}) + +export class ModerationBehaviorSuiteRunner { + constructor(public suite: ModerationBehaviors) {} + + postScenario(scenario: ModerationBehaviorScenario) { + if (scenario.subject !== 'post') { + throw new Error('Scenario subject must be "post"') + } + const author = this.profileViewBasic(scenario.author, scenario.labels) + return m.postView({ + record: m.post({ + text: 'Post text', + }), + author, + labels: (scenario.labels.post || []).map((val) => + m.label({ val, uri: `at://${author.did}/app.bsky.feed.post/fake` }), + ), + embed: scenario.quoteAuthor + ? m.embedRecordView({ + record: m.post({ + text: 'Quoted post text', + }), + labels: (scenario.labels.quotedPost || []).map((val) => + m.label({ + val, + uri: `at://${author.did}/app.bsky.feed.post/fake`, + }), + ), + author: this.profileViewBasic(scenario.quoteAuthor, { + account: scenario.labels.quotedAccount, + }), + }) + : undefined, + }) + } + + profileScenario(scenario: ModerationBehaviorScenario) { + if (scenario.subject !== 'profile') { + throw new Error('Scenario subject must be "profile"') + } + return this.profileViewBasic(scenario.author, scenario.labels) + } + + profileViewBasic( + name: string, + scenarioLabels: ModerationBehaviorScenario['labels'], + ) { + const def = this.suite.users[name] + + const labels: ComAtprotoLabelDefs.Label[] = [] + if (scenarioLabels.account) { + for (const l of scenarioLabels.account) { + labels.push(m.label({ val: l, uri: `did:web:${name}` })) + } + } + if (scenarioLabels.profile) { + for (const l of scenarioLabels.profile) { + labels.push( + m.label({ + val: l, + uri: `at://did:web:${name}/app.bsky.actor.profile/self`, + }), + ) + } + } + + return m.profileViewBasic({ + handle: `${name}.test`, + labels, + viewer: m.actorViewerState({ + muted: def.muted || def.mutedByList, + mutedByList: def.mutedByList + ? m.listViewBasic({ name: 'Fake List' }) + : undefined, + blockedBy: def.blockedBy, + blocking: def.blocking + ? 'at://did:web:self.test/app.bsky.graph.block/fake' + : undefined, + }), + }) + } + + moderationOpts(scenario: ModerationBehaviorScenario): ModerationOpts { + return { + userDid: 'did:web:self.test', + adultContentEnabled: Boolean( + this.suite.configurations[scenario.cfg].adultContentEnabled, + ), + labelerSettings: [ + { + labeler: { + did: 'did:plc:fake-labeler', + displayName: 'Fake Labeler', + }, + settings: this.suite.configurations[scenario.cfg].settings, + }, + ], + } + } +} diff --git a/yarn.lock b/yarn.lock index 0bc37b7b7eb..b419b3adca4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5935,6 +5935,11 @@ commander@^9.4.0: resolved "https://registry.npmjs.org/commander/-/commander-9.4.0.tgz" integrity sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw== +common-tags@^1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" + integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== + compare-func@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz" From 749db2e425903431c0d5a11780162d5a0a06140a Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 3 Aug 2023 10:29:54 -0700 Subject: [PATCH 084/237] @atproto/api@0.5.0 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 13708f98b50..57f202b7348 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.4.4", + "version": "0.5.0", "main": "src/index.ts", "scripts": { "codegen": "yarn docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From ce5a43067db61d06e5ae6789da6442aa005b8987 Mon Sep 17 00:00:00 2001 From: dholms Date: Thu, 3 Aug 2023 12:56:30 -0500 Subject: [PATCH 085/237] v0.5.1 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 57f202b7348..1a246ac70b5 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.5.0", + "version": "0.5.1", "main": "src/index.ts", "scripts": { "codegen": "yarn docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From 6d04bf76ef026d44a783e0fde9eedfc55542c6f6 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 3 Aug 2023 17:42:05 -0500 Subject: [PATCH 086/237] Hive retry logic & logging (#1434) * better logging & retry logic for hive * tidy * fix test * bump timeout * tidy pds labeler * fix test * build branch * fix types * fix types * rm branch builds * log hive response --------- Co-authored-by: Devin Ivy --- packages/bsky/src/api/blob-resolver.ts | 2 +- packages/bsky/src/indexer/index.ts | 9 +++- packages/bsky/src/labeler/base.ts | 10 ++-- packages/bsky/src/labeler/hive.ts | 51 ++++++++++----------- packages/bsky/tests/labeler/labeler.test.ts | 20 ++++---- packages/pds/src/labeler/base.ts | 9 ++-- packages/pds/src/labeler/hive.ts | 6 ++- packages/pds/tests/labeler/labeler.test.ts | 18 +++++--- 8 files changed, 67 insertions(+), 58 deletions(-) diff --git a/packages/bsky/src/api/blob-resolver.ts b/packages/bsky/src/api/blob-resolver.ts index c19f3d34f3e..008bf4bb6a0 100644 --- a/packages/bsky/src/api/blob-resolver.ts +++ b/packages/bsky/src/api/blob-resolver.ts @@ -123,6 +123,6 @@ async function getBlob(opts: { pds: string; did: string; cid: string }) { params: { did, cid }, decompress: true, responseType: 'stream', - timeout: 2000, // 2sec of inactivity on the connection + timeout: 5000, // 5sec of inactivity on the connection }) } diff --git a/packages/bsky/src/indexer/index.ts b/packages/bsky/src/indexer/index.ts index c50654975a4..ccb26c3311e 100644 --- a/packages/bsky/src/indexer/index.ts +++ b/packages/bsky/src/indexer/index.ts @@ -74,7 +74,7 @@ export class BskyIndexer { } async start() { - const { db } = this.ctx + const { db, backgroundQueue } = this.ctx const { pool } = db.cfg this.dbStatsInterval = setInterval(() => { dbLogger.info( @@ -85,6 +85,13 @@ export class BskyIndexer { }, 'db pool stats', ) + dbLogger.info( + { + runningCount: backgroundQueue.queue.pending, + waitingCount: backgroundQueue.queue.size, + }, + 'background queue stats', + ) }, 10000) this.subStatsInterval = setInterval(() => { log.info( diff --git a/packages/bsky/src/labeler/base.ts b/packages/bsky/src/labeler/base.ts index 8253ba9a7a2..71e935b6f87 100644 --- a/packages/bsky/src/labeler/base.ts +++ b/packages/bsky/src/labeler/base.ts @@ -1,15 +1,14 @@ -import stream from 'stream' import { AtUri } from '@atproto/uri' import { AtpAgent } from '@atproto/api' import { cidForRecord } from '@atproto/repo' import { dedupe, getFieldsFromRecord } from './util' import { labelerLogger as log } from '../logger' -import { resolveBlob } from '../api/blob-resolver' import Database from '../db' import { IdResolver } from '@atproto/identity' import { BackgroundQueue } from '../background' import { IndexerConfig } from '../indexer/config' import { buildBasicAuth } from '../auth' +import { CID } from 'multiformats/cid' export abstract class Labeler { public backgroundQueue: BackgroundQueue @@ -85,16 +84,13 @@ export abstract class Labeler { const { text, imgs } = getFieldsFromRecord(obj) const txtLabels = await this.labelText(text.join(' ')) const imgLabels = await Promise.all( - imgs.map(async (cid) => { - const { stream } = await resolveBlob(uri.host, cid, this.ctx) - return this.labelImg(stream) - }), + imgs.map((cid) => this.labelImg(uri.host, cid)), ) return dedupe([...txtLabels, ...imgLabels.flat()]) } abstract labelText(text: string): Promise - abstract labelImg(img: stream.Readable): Promise + abstract labelImg(did: string, cid: CID): Promise async processAll() { await this.backgroundQueue.processAll() diff --git a/packages/bsky/src/labeler/hive.ts b/packages/bsky/src/labeler/hive.ts index 42bf70dcc13..e78539dfa1d 100644 --- a/packages/bsky/src/labeler/hive.ts +++ b/packages/bsky/src/labeler/hive.ts @@ -1,13 +1,15 @@ -import stream from 'stream' import axios from 'axios' import FormData from 'form-data' +import { CID } from 'multiformats/cid' +import { IdResolver } from '@atproto/identity' import { Labeler } from './base' import { keywordLabeling } from './util' -import { IdResolver } from '@atproto/identity' import Database from '../db' import { BackgroundQueue } from '../background' import { IndexerConfig } from '../indexer/config' import { retryHttp } from '../util/retry' +import { resolveBlob } from '../api/blob-resolver' +import { labelerLogger as log } from '../logger' const HIVE_ENDPOINT = 'https://api.thehive.ai/api/v2/task/sync' @@ -33,36 +35,33 @@ export class HiveLabeler extends Labeler { return keywordLabeling(this.keywords, text) } - async labelImg(img: stream.Readable): Promise { - return labelBlob(img, this.hiveApiKey) + async labelImg(did: string, cid: CID): Promise { + const hiveRes = await retryHttp(async () => { + try { + return await this.makeHiveReq(did, cid) + } catch (err) { + log.warn({ err, did, cid: cid.toString() }, 'hive request failed') + throw err + } + }) + log.info({ hiveRes, did, cid: cid.toString() }, 'hive response') + const classes = respToClasses(hiveRes) + return summarizeLabels(classes) } -} -export const labelBlob = async ( - blob: stream.Readable, - hiveApiKey: string, -): Promise => { - const classes = await makeHiveReq(blob, hiveApiKey) - return summarizeLabels(classes) -} - -export const makeHiveReq = async ( - blob: stream.Readable, - hiveApiKey: string, -): Promise => { - const form = new FormData() - form.append('media', blob) - - const res = await retryHttp(() => - axios.post(HIVE_ENDPOINT, form, { + async makeHiveReq(did: string, cid: CID): Promise { + const { stream } = await resolveBlob(did, cid, this.ctx) + const form = new FormData() + form.append('media', stream) + const { data } = await axios.post(HIVE_ENDPOINT, form, { headers: { 'Content-Type': 'multipart/form-data', - authorization: `token ${hiveApiKey}`, + authorization: `token ${this.hiveApiKey}`, accept: 'application/json', }, - }), - ) - return respToClasses(res.data) + }) + return data + } } export const respToClasses = (res: HiveResp): HiveRespClass[] => { diff --git a/packages/bsky/tests/labeler/labeler.test.ts b/packages/bsky/tests/labeler/labeler.test.ts index 58d11765987..18d0e5e0daa 100644 --- a/packages/bsky/tests/labeler/labeler.test.ts +++ b/packages/bsky/tests/labeler/labeler.test.ts @@ -1,18 +1,22 @@ import { AtUri, AtpAgent, BlobRef } from '@atproto/api' -import stream, { Readable } from 'stream' +import { Readable } from 'stream' import { Labeler } from '../../src/labeler' import { Database, IndexerConfig } from '../../src' import IndexerContext from '../../src/indexer/context' import { cidForRecord } from '@atproto/repo' import { keywordLabeling } from '../../src/labeler/util' -import { cidForCbor, streamToBytes, TID } from '@atproto/common' -import * as ui8 from 'uint8arrays' +import { cidForCbor, TID } from '@atproto/common' import { LabelService } from '../../src/services/label' import { TestNetwork } from '@atproto/dev-env' import { IdResolver } from '@atproto/identity' import { SeedClient } from '../seeds/client' import usersSeed from '../seeds/users' import { BackgroundQueue } from '../../src/background' +import { CID } from 'multiformats/cid' + +// outside of test suite so that TestLabeler can access them +let badCid1: CID | undefined = undefined +let badCid2: CID | undefined = undefined describe('labeler', () => { let network: TestNetwork @@ -67,6 +71,8 @@ describe('labeler', () => { badBlob1 = await storeBlob(bytes1) badBlob2 = await storeBlob(bytes2) goodBlob = await storeBlob(bytes3) + badCid1 = badBlob1.ref + badCid2 = badBlob2.ref }) afterAll(async () => { @@ -171,13 +177,11 @@ class TestLabeler extends Labeler { return keywordLabeling(this.keywords, text) } - async labelImg(img: stream.Readable): Promise { - const buf = await streamToBytes(img) - if (ui8.equals(buf, new Uint8Array([1, 2, 3, 4]))) { + async labelImg(_did: string, cid: CID): Promise { + if (cid.equals(badCid1)) { return ['img-label'] } - - if (ui8.equals(buf, new Uint8Array([5, 6, 7, 8]))) { + if (cid.equals(badCid2)) { return ['other-img-label'] } return [] diff --git a/packages/pds/src/labeler/base.ts b/packages/pds/src/labeler/base.ts index c2e54513fba..23479e9ff6f 100644 --- a/packages/pds/src/labeler/base.ts +++ b/packages/pds/src/labeler/base.ts @@ -1,10 +1,10 @@ -import stream from 'stream' import Database from '../db' import { BlobStore, cidForRecord } from '@atproto/repo' import { dedupe, getFieldsFromRecord } from './util' import { AtUri } from '@atproto/uri' import { labelerLogger as log } from '../logger' import { BackgroundQueue } from '../event-stream/background-queue' +import { CID } from 'multiformats/cid' export abstract class Labeler { public db: Database @@ -58,16 +58,13 @@ export abstract class Labeler { const { text, imgs } = getFieldsFromRecord(obj) const txtLabels = await this.labelText(text.join(' ')) const imgLabels = await Promise.all( - imgs.map(async (cid) => { - const stream = await this.blobstore.getStream(cid) - return this.labelImg(stream) - }), + imgs.map(async (cid) => this.labelImg(cid)), ) return dedupe([...txtLabels, ...imgLabels.flat()]) } abstract labelText(text: string): Promise - abstract labelImg(img: stream.Readable): Promise + abstract labelImg(cid: CID): Promise async processAll() { await this.backgroundQueue.processAll() diff --git a/packages/pds/src/labeler/hive.ts b/packages/pds/src/labeler/hive.ts index 41bddf572a4..bdbe70609bd 100644 --- a/packages/pds/src/labeler/hive.ts +++ b/packages/pds/src/labeler/hive.ts @@ -6,6 +6,7 @@ import Database from '../db' import { BlobStore } from '@atproto/repo' import { keywordLabeling } from './util' import { BackgroundQueue } from '../event-stream/background-queue' +import { CID } from 'multiformats/cid' const HIVE_ENDPOINT = 'https://api.thehive.ai/api/v2/task/sync' @@ -32,8 +33,9 @@ export class HiveLabeler extends Labeler { return keywordLabeling(this.keywords, text) } - async labelImg(img: stream.Readable): Promise { - return labelBlob(img, this.hiveApiKey) + async labelImg(cid: CID): Promise { + const stream = await this.blobstore.getStream(cid) + return labelBlob(stream, this.hiveApiKey) } } diff --git a/packages/pds/tests/labeler/labeler.test.ts b/packages/pds/tests/labeler/labeler.test.ts index 2525732357a..1a3c5295359 100644 --- a/packages/pds/tests/labeler/labeler.test.ts +++ b/packages/pds/tests/labeler/labeler.test.ts @@ -1,14 +1,17 @@ import { AtUri, BlobRef } from '@atproto/api' -import stream from 'stream' import { runTestServer, CloseFn, TestServerInfo } from '../_util' import { Labeler } from '../../src/labeler' import { AppContext, Database } from '../../src' import { BlobStore, cidForRecord } from '@atproto/repo' import { keywordLabeling } from '../../src/labeler/util' -import { cidForCbor, streamToBytes, TID } from '@atproto/common' -import * as ui8 from 'uint8arrays' +import { cidForCbor, TID } from '@atproto/common' import { LabelService } from '../../src/app-view/services/label' import { BackgroundQueue } from '../../src/event-stream/background-queue' +import { CID } from 'multiformats/cid' + +// outside of test suite so that TestLabeler can access them +let badCid1: CID | undefined = undefined +let badCid2: CID | undefined = undefined describe('labeler', () => { let server: TestServerInfo @@ -48,6 +51,8 @@ describe('labeler', () => { badBlob1 = new BlobRef(cid1, 'image/jpeg', 4) badBlob2 = new BlobRef(cid2, 'image/jpeg', 4) goodBlob = new BlobRef(cid3, 'image/jpeg', 4) + badCid1 = badBlob1.ref + badCid2 = badBlob2.ref }) afterAll(async () => { @@ -160,13 +165,12 @@ class TestLabeler extends Labeler { return keywordLabeling(this.keywords, text) } - async labelImg(img: stream.Readable): Promise { - const buf = await streamToBytes(img) - if (ui8.equals(buf, new Uint8Array([1, 2, 3, 4]))) { + async labelImg(cid: CID): Promise { + if (cid.equals(badCid1)) { return ['img-label'] } - if (ui8.equals(buf, new Uint8Array([5, 6, 7, 8]))) { + if (cid.equals(badCid2)) { return ['other-img-label'] } return [] From 82ada3d27aec8d2491bf8698b7dac17e04265428 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 3 Aug 2023 21:54:06 -0700 Subject: [PATCH 087/237] Moderation API fixes - QP blurs & label names (#1435) * Fix: blur quote posts with the blur-media label type * Switch to label titles that are less stilted --- packages/api/definitions/locale/en/labels.json | 12 ++++++------ .../api/definitions/post-moderation-behaviors.json | 7 +++++-- packages/api/docs/labels.md | 12 ++++++------ packages/api/docs/moderation-behaviors/posts.md | 6 +++--- packages/api/src/moderation/const/labels.ts | 12 ++++++------ packages/api/src/moderation/index.ts | 3 +++ 6 files changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/api/definitions/locale/en/labels.json b/packages/api/definitions/locale/en/labels.json index f4832212e52..8d0fa82df52 100644 --- a/packages/api/definitions/locale/en/labels.json +++ b/packages/api/definitions/locale/en/labels.json @@ -75,11 +75,11 @@ "description": "Images of full-frontal nudity (genitalia) in any sexualized context, or explicit sexual activity (meaning contact with genitalia or breasts) even if partially covered. Includes graphic sexual cartoons (often jokes/memes)." }, "account": { - "name": "Pornography", + "name": "Adult Content", "description": "This account contains imagery of full-frontal nudity or explicit sexual activity." }, "content": { - "name": "Pornography", + "name": "Adult Content", "description": "This content contains imagery of full-frontal nudity or explicit sexual activity." } }, @@ -89,11 +89,11 @@ "description": "Content that does not meet the level of \"pornography\", but is still sexual. Some common examples have been selfies and \"hornyposting\" with underwear on, or partially naked (naked but covered, eg with hands or from side perspective). Sheer/see-through nipples may end up in this category." }, "account": { - "name": "Sexually Suggestive", + "name": "Suggestive Content", "description": "This account contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress." }, "content": { - "name": "Sexually Suggestive", + "name": "Suggestive Content", "description": "This content contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress." } }, @@ -103,11 +103,11 @@ "description": "Nudity which is not sexual, or that is primarily \"artistic\" in nature. For example: breastfeeding; classic art paintings and sculptures; newspaper images with some nudity; fashion modeling. \"Erotic photography\" is likely to end up in sexual or porn." }, "account": { - "name": "Nudity", + "name": "Adult Content", "description": "This account contains imagery which portrays nudity in a non-sexual or artistic setting." }, "content": { - "name": "Nudity", + "name": "Adult Content", "description": "This content contains imagery which portrays nudity in a non-sexual or artistic setting." } }, diff --git a/packages/api/definitions/post-moderation-behaviors.json b/packages/api/definitions/post-moderation-behaviors.json index 9b381d96c8e..e7aec1788c3 100644 --- a/packages/api/definitions/post-moderation-behaviors.json +++ b/packages/api/definitions/post-moderation-behaviors.json @@ -379,7 +379,8 @@ "quoteAuthor": "alice", "labels": {"quotedPost": ["porn"]}, "behaviors": { - "content": {"cause": "label:porn", "filter": true} + "content": {"cause": "label:porn", "filter": true}, + "embed": {"cause": "label:porn", "blur": true} } }, "Blur-media label ('porn') on quoted author account (hide)": { @@ -428,6 +429,7 @@ "quoteAuthor": "alice", "labels": {"quotedPost": ["porn"]}, "behaviors": { + "embed": {"cause": "label:porn", "blur": true} } }, "Blur-media label ('porn') on quoted author account (warn)": { @@ -653,7 +655,8 @@ "quoteAuthor": "alice", "labels": {"quotedPost": ["porn"]}, "behaviors": { - "content": {"cause": "label:porn", "filter": true} + "content": {"cause": "label:porn", "filter": true}, + "embed": {"cause": "label:porn", "blur": true, "noOverride": true} } }, "Adult-only label on quoted author account when adult content is disabled": { diff --git a/packages/api/docs/labels.md b/packages/api/docs/labels.md index 03f003a85fb..a47d3921ac3 100644 --- a/packages/api/docs/labels.md +++ b/packages/api/docs/labels.md @@ -355,24 +355,24 @@ porn general
Pornography
Images of full-frontal nudity (genitalia) in any sexualized context, or explicit sexual activity (meaning contact with genitalia or breasts) even if partially covered. Includes graphic sexual cartoons (often jokes/memes).

- on an account
Pornography
This account contains imagery of full-frontal nudity or explicit sexual activity.

- on content
Pornography
This content contains imagery of full-frontal nudity or explicit sexual activity.

+ on an account
Adult Content
This account contains imagery of full-frontal nudity or explicit sexual activity.

+ on content
Adult Content
This content contains imagery of full-frontal nudity or explicit sexual activity.

sexual general
Sexually Suggestive
Content that does not meet the level of "pornography", but is still sexual. Some common examples have been selfies and "hornyposting" with underwear on, or partially naked (naked but covered, eg with hands or from side perspective). Sheer/see-through nipples may end up in this category.

- on an account
Sexually Suggestive
This account contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress.

- on content
Sexually Suggestive
This content contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress.

+ on an account
Suggestive Content
This account contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress.

+ on content
Suggestive Content
This content contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress.

nudity general
Nudity
Nudity which is not sexual, or that is primarily "artistic" in nature. For example: breastfeeding; classic art paintings and sculptures; newspaper images with some nudity; fashion modeling. "Erotic photography" is likely to end up in sexual or porn.

- on an account
Nudity
This account contains imagery which portrays nudity in a non-sexual or artistic setting.

- on content
Nudity
This content contains imagery which portrays nudity in a non-sexual or artistic setting.

+ on an account
Adult Content
This account contains imagery which portrays nudity in a non-sexual or artistic setting.

+ on content
Adult Content
This content contains imagery which portrays nudity in a non-sexual or artistic setting.

diff --git a/packages/api/docs/moderation-behaviors/posts.md b/packages/api/docs/moderation-behaviors/posts.md index 9ad384c7400..9b77bbc73bb 100644 --- a/packages/api/docs/moderation-behaviors/posts.md +++ b/packages/api/docs/moderation-behaviors/posts.md @@ -767,7 +767,7 @@ Key: - +✋ @@ -877,7 +877,7 @@ Key: - +✋ @@ -1427,7 +1427,7 @@ Key: - +🚫 diff --git a/packages/api/src/moderation/const/labels.ts b/packages/api/src/moderation/const/labels.ts index da12706d442..59f36cd842f 100644 --- a/packages/api/src/moderation/const/labels.ts +++ b/packages/api/src/moderation/const/labels.ts @@ -167,14 +167,14 @@ export const LABELS: LabelDefinitionMap = { }, account: { en: { - name: 'Pornography', + name: 'Adult Content', description: 'This account contains imagery of full-frontal nudity or explicit sexual activity.', }, }, content: { en: { - name: 'Pornography', + name: 'Adult Content', description: 'This content contains imagery of full-frontal nudity or explicit sexual activity.', }, @@ -198,14 +198,14 @@ export const LABELS: LabelDefinitionMap = { }, account: { en: { - name: 'Sexually Suggestive', + name: 'Suggestive Content', description: 'This account contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress.', }, }, content: { en: { - name: 'Sexually Suggestive', + name: 'Suggestive Content', description: 'This content contains imagery which is sexually suggestive. Common examples include selfies in underwear or in partial undress.', }, @@ -229,14 +229,14 @@ export const LABELS: LabelDefinitionMap = { }, account: { en: { - name: 'Nudity', + name: 'Adult Content', description: 'This account contains imagery which portrays nudity in a non-sexual or artistic setting.', }, }, content: { en: { - name: 'Nudity', + name: 'Adult Content', description: 'This content contains imagery which portrays nudity in a non-sexual or artistic setting.', }, diff --git a/packages/api/src/moderation/index.ts b/packages/api/src/moderation/index.ts index 04edfc3e600..d659783b67a 100644 --- a/packages/api/src/moderation/index.ts +++ b/packages/api/src/moderation/index.ts @@ -148,6 +148,9 @@ export function moderatePost( quote = decideQuotedPostWithMedia(subject.embed, opts) quotedAccount = decideQuotedPostWithMediaAccount(subject.embed, opts) } + if (quote?.blurMedia) { + quote.blur = true // treat blurMedia of quote as blur of quote + } // downgrade based on authorship if (!isModerationDecisionNoop(post) && post.did === opts.userDid) { From 43bb6ae1476efd27ca8e068428e43054f983f92a Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 3 Aug 2023 21:54:38 -0700 Subject: [PATCH 088/237] @atproto/api@0.5.2 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 1a246ac70b5..0d568647695 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.5.1", + "version": "0.5.2", "main": "src/index.ts", "scripts": { "codegen": "yarn docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From 3f3ae4cfd50b3e27c43a257d68d7d4992ee29739 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Fri, 4 Aug 2023 16:06:04 -0400 Subject: [PATCH 089/237] Support proxying moderation through to appview (#1233) * in-progress work on proxying moderation to appview * tidy * proxy reports pds to appview, misc tidy * test proxying of moderation endpoints * remove report action fkeys from pds for appview sync * test appview/pds moderation synchronization * tidy * tidy * fix admin proxy tests, build * temp disable migration * rm relative @atproto/api imports * fix a couple more relative imports * tidy * reenable migration, comment contents temporarily * fully enable migration, remove build, misc test fixes --------- Co-authored-by: dholms --- .../bsky/src/api/app/bsky/feed/getPosts.ts | 2 +- .../com/atproto/admin/getModerationAction.ts | 16 +- .../com/atproto/admin/getModerationActions.ts | 15 +- .../com/atproto/admin/getModerationReport.ts | 15 +- .../com/atproto/admin/getModerationReports.ts | 15 +- .../src/api/com/atproto/admin/getRecord.ts | 43 +- .../pds/src/api/com/atproto/admin/getRepo.ts | 41 +- .../atproto/admin/resolveModerationReports.ts | 15 +- .../atproto/admin/reverseModerationAction.ts | 55 ++- .../src/api/com/atproto/admin/searchRepos.ts | 17 +- .../com/atproto/admin/takeModerationAction.ts | 62 ++- .../pds/src/api/com/atproto/admin/util.ts | 41 ++ .../com/atproto/moderation/createReport.ts | 18 +- .../api/app/bsky/feed/getAuthorFeed.ts | 8 +- .../app-view/api/app/bsky/feed/getPosts.ts | 2 +- packages/pds/src/config.ts | 8 + packages/pds/src/context.ts | 7 + ...0801T195109532Z-remove-moderation-fkeys.ts | 56 +++ packages/pds/src/db/migrations/index.ts | 1 + .../pds/tests/labeler/apply-labels.test.ts | 2 +- packages/pds/tests/labeler/labeler.test.ts | 2 +- .../proxied/__snapshots__/admin.test.ts.snap | 428 ++++++++++++++++++ packages/pds/tests/proxied/admin.test.ts | 405 +++++++++++++++++ packages/pds/tests/seeds/basic.ts | 4 +- packages/pds/tests/seeds/users.ts | 10 +- 25 files changed, 1250 insertions(+), 38 deletions(-) create mode 100644 packages/pds/src/api/com/atproto/admin/util.ts create mode 100644 packages/pds/src/db/migrations/20230801T195109532Z-remove-moderation-fkeys.ts create mode 100644 packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap create mode 100644 packages/pds/tests/proxied/admin.test.ts diff --git a/packages/bsky/src/api/app/bsky/feed/getPosts.ts b/packages/bsky/src/api/app/bsky/feed/getPosts.ts index 505eb25d739..615cff616ed 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPosts.ts @@ -1,7 +1,7 @@ import * as common from '@atproto/common' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { PostView } from '@atproto/api/src/client/types/app/bsky/feed/defs' +import { PostView } from '../../../../lexicon/types/app/bsky/feed/defs' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getPosts({ diff --git a/packages/pds/src/api/com/atproto/admin/getModerationAction.ts b/packages/pds/src/api/com/atproto/admin/getModerationAction.ts index 5e6ee8113c2..6005816999a 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationAction.ts @@ -1,10 +1,24 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { authPassthru } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationAction({ auth: ctx.roleVerifier, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { + if (ctx.shouldProxyModeration()) { + // @TODO merge invite details into action subject + const { data: result } = + await ctx.appviewAgent.com.atproto.admin.getModerationAction( + params, + authPassthru(req), + ) + return { + encoding: 'application/json', + body: result, + } + } + const access = auth.credentials const { db, services } = ctx const { id } = params diff --git a/packages/pds/src/api/com/atproto/admin/getModerationActions.ts b/packages/pds/src/api/com/atproto/admin/getModerationActions.ts index 2231eb686c3..5467b590ea5 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationActions.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationActions.ts @@ -1,10 +1,23 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { authPassthru } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationActions({ auth: ctx.roleVerifier, - handler: async ({ params }) => { + handler: async ({ req, params }) => { + if (ctx.shouldProxyModeration()) { + const { data: result } = + await ctx.appviewAgent.com.atproto.admin.getModerationActions( + params, + authPassthru(req), + ) + return { + encoding: 'application/json', + body: result, + } + } + const { db, services } = ctx const { subject, limit = 50, cursor } = params const moderationService = services.moderation(db) diff --git a/packages/pds/src/api/com/atproto/admin/getModerationReport.ts b/packages/pds/src/api/com/atproto/admin/getModerationReport.ts index 66d47f01fa7..0368a337c28 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationReport.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationReport.ts @@ -1,10 +1,23 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { authPassthru } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationReport({ auth: ctx.roleVerifier, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { + if (ctx.shouldProxyModeration()) { + const { data: result } = + await ctx.appviewAgent.com.atproto.admin.getModerationReport( + params, + authPassthru(req), + ) + return { + encoding: 'application/json', + body: result, + } + } + const access = auth.credentials const { db, services } = ctx const { id } = params diff --git a/packages/pds/src/api/com/atproto/admin/getModerationReports.ts b/packages/pds/src/api/com/atproto/admin/getModerationReports.ts index ce7cc936e83..8ef2b69c319 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationReports.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationReports.ts @@ -1,10 +1,23 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { authPassthru } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationReports({ auth: ctx.roleVerifier, - handler: async ({ params }) => { + handler: async ({ req, params }) => { + if (ctx.shouldProxyModeration()) { + const { data: result } = + await ctx.appviewAgent.com.atproto.admin.getModerationReports( + params, + authPassthru(req), + ) + return { + encoding: 'application/json', + body: result, + } + } + const { db, services } = ctx const { subject, diff --git a/packages/pds/src/api/com/atproto/admin/getRecord.ts b/packages/pds/src/api/com/atproto/admin/getRecord.ts index 53164c5c6e0..92489b1c1f0 100644 --- a/packages/pds/src/api/com/atproto/admin/getRecord.ts +++ b/packages/pds/src/api/com/atproto/admin/getRecord.ts @@ -1,26 +1,57 @@ +import { AtUri } from '@atproto/uri' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { AtUri } from '@atproto/uri' +import { authPassthru, mergeRepoViewPdsDetails } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getRecord({ auth: ctx.roleVerifier, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const access = auth.credentials const { db, services } = ctx const { uri, cid } = params const result = await services .record(db) .getRecord(new AtUri(uri), cid ?? null, true) - if (!result) { + const recordDetail = + result && + (await services.moderation(db).views.recordDetail(result, { + includeEmails: access.moderator, + })) + + if (ctx.shouldProxyModeration()) { + try { + const { data: recordDetailAppview } = + await ctx.appviewAgent.com.atproto.admin.getRecord( + params, + authPassthru(req), + ) + if (recordDetail) { + recordDetailAppview.repo = mergeRepoViewPdsDetails( + recordDetailAppview.repo, + recordDetail.repo, + ) + } + return { + encoding: 'application/json', + body: recordDetailAppview, + } + } catch (err) { + if (err && err['error'] === 'RecordNotFound') { + throw new InvalidRequestError('Record not found', 'RecordNotFound') + } else { + throw err + } + } + } + + if (!recordDetail) { throw new InvalidRequestError('Record not found', 'RecordNotFound') } return { encoding: 'application/json', - body: await services.moderation(db).views.recordDetail(result, { - includeEmails: access.moderator, - }), + body: recordDetail, } }, }) diff --git a/packages/pds/src/api/com/atproto/admin/getRepo.ts b/packages/pds/src/api/com/atproto/admin/getRepo.ts index 476c762bb5e..b3a0f107bc8 100644 --- a/packages/pds/src/api/com/atproto/admin/getRepo.ts +++ b/packages/pds/src/api/com/atproto/admin/getRepo.ts @@ -1,23 +1,54 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { authPassthru, mergeRepoViewPdsDetails } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getRepo({ auth: ctx.roleVerifier, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { const access = auth.credentials const { db, services } = ctx const { did } = params const result = await services.account(db).getAccount(did, true) - if (!result) { + const repoDetail = + result && + (await services.moderation(db).views.repoDetail(result, { + includeEmails: access.moderator, + })) + + if (ctx.shouldProxyModeration()) { + try { + let { data: repoDetailAppview } = + await ctx.appviewAgent.com.atproto.admin.getRepo( + params, + authPassthru(req), + ) + if (repoDetail) { + repoDetailAppview = mergeRepoViewPdsDetails( + repoDetailAppview, + repoDetail, + ) + } + return { + encoding: 'application/json', + body: repoDetailAppview, + } + } catch (err) { + if (err && err['error'] === 'RepoNotFound') { + throw new InvalidRequestError('Repo not found', 'RepoNotFound') + } else { + throw err + } + } + } + + if (!repoDetail) { throw new InvalidRequestError('Repo not found', 'RepoNotFound') } return { encoding: 'application/json', - body: await services.moderation(db).views.repoDetail(result, { - includeEmails: access.moderator, - }), + body: repoDetail, } }, }) diff --git a/packages/pds/src/api/com/atproto/admin/resolveModerationReports.ts b/packages/pds/src/api/com/atproto/admin/resolveModerationReports.ts index 6ec217b0655..926246f2d60 100644 --- a/packages/pds/src/api/com/atproto/admin/resolveModerationReports.ts +++ b/packages/pds/src/api/com/atproto/admin/resolveModerationReports.ts @@ -1,10 +1,23 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { authPassthru } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.resolveModerationReports({ auth: ctx.roleVerifier, - handler: async ({ input }) => { + handler: async ({ req, input }) => { + if (ctx.shouldProxyModeration()) { + const { data: result } = + await ctx.appviewAgent.com.atproto.admin.resolveModerationReports( + input.body, + authPassthru(req, true), + ) + return { + encoding: 'application/json', + body: result, + } + } + const { db, services } = ctx const moderationService = services.moderation(db) const { actionId, reportIds, createdBy } = input.body diff --git a/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts b/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts index 05991968d02..c891424b1c2 100644 --- a/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts @@ -3,17 +3,70 @@ import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { + isRepoRef, ACKNOWLEDGE, ESCALATE, TAKEDOWN, } from '../../../../lexicon/types/com/atproto/admin/defs' +import { isMain as isStrongRef } from '../../../../lexicon/types/com/atproto/repo/strongRef' +import { authPassthru } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.reverseModerationAction({ auth: ctx.roleVerifier, - handler: async ({ input, auth }) => { + handler: async ({ req, input, auth }) => { const access = auth.credentials const { db, services } = ctx + if (ctx.shouldProxyModeration()) { + const { data: result } = + await ctx.appviewAgent.com.atproto.admin.reverseModerationAction( + input.body, + authPassthru(req, true), + ) + + const transact = db.transaction(async (dbTxn) => { + const moderationTxn = services.moderation(dbTxn) + const labelTxn = services.appView.label(dbTxn) + // reverse takedowns + if (result.action === TAKEDOWN && isRepoRef(result.subject)) { + await moderationTxn.reverseTakedownRepo({ + did: result.subject.did, + }) + } + if (result.action === TAKEDOWN && isStrongRef(result.subject)) { + await moderationTxn.reverseTakedownRecord({ + uri: new AtUri(result.subject.uri), + }) + } + // invert label creation & negations + const reverseLabels = (uri: string, cid: string | null) => + labelTxn.formatAndCreate(ctx.cfg.labelerDid, uri, cid, { + create: result.negateLabelVals, + negate: result.createLabelVals, + }) + if (isRepoRef(result.subject)) { + await reverseLabels(result.subject.did, null) + } + if (isStrongRef(result.subject)) { + await reverseLabels(result.subject.uri, result.subject.cid) + } + }) + + try { + await transact + } catch (err) { + req.log.error( + { err, actionId: input.body.id }, + 'proxied moderation action reversal failed', + ) + } + + return { + encoding: 'application/json', + body: result, + } + } + const moderationService = services.moderation(db) const { id, createdBy, reason } = input.body diff --git a/packages/pds/src/api/com/atproto/admin/searchRepos.ts b/packages/pds/src/api/com/atproto/admin/searchRepos.ts index 28a49b947c3..42ec5aa4058 100644 --- a/packages/pds/src/api/com/atproto/admin/searchRepos.ts +++ b/packages/pds/src/api/com/atproto/admin/searchRepos.ts @@ -3,11 +3,26 @@ import AppContext from '../../../../context' import { SearchKeyset } from '../../../../services/util/search' import { sql } from 'kysely' import { ListKeyset } from '../../../../services/account' +import { authPassthru } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.searchRepos({ auth: ctx.roleVerifier, - handler: async ({ params, auth }) => { + handler: async ({ req, params, auth }) => { + if (ctx.shouldProxyModeration()) { + // @TODO merge invite details to this list view. could also add + // support for invitedBy param, which is not supported by appview. + const { data: result } = + await ctx.appviewAgent.com.atproto.admin.searchRepos( + params, + authPassthru(req), + ) + return { + encoding: 'application/json', + body: result, + } + } + const access = auth.credentials const { db, services } = ctx const moderationService = services.moderation(db) diff --git a/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts b/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts index 1a8feedd98b..e6f1f76b737 100644 --- a/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts @@ -1,21 +1,79 @@ import { CID } from 'multiformats/cid' import { AtUri } from '@atproto/uri' -import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { + isRepoRef, ACKNOWLEDGE, ESCALATE, TAKEDOWN, } from '../../../../lexicon/types/com/atproto/admin/defs' +import { isMain as isStrongRef } from '../../../../lexicon/types/com/atproto/repo/strongRef' import { getSubject, getAction } from '../moderation/util' +import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' +import { authPassthru } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.takeModerationAction({ auth: ctx.roleVerifier, - handler: async ({ input, auth }) => { + handler: async ({ req, input, auth }) => { const access = auth.credentials const { db, services } = ctx + if (ctx.shouldProxyModeration()) { + const { data: result } = + await ctx.appviewAgent.com.atproto.admin.takeModerationAction( + input.body, + authPassthru(req, true), + ) + + const transact = db.transaction(async (dbTxn) => { + const authTxn = services.auth(dbTxn) + const moderationTxn = services.moderation(dbTxn) + const labelTxn = services.appView.label(dbTxn) + // perform takedowns + if (result.action === TAKEDOWN && isRepoRef(result.subject)) { + await authTxn.revokeRefreshTokensByDid(result.subject.did) + await moderationTxn.takedownRepo({ + takedownId: result.id, + did: result.subject.did, + }) + } + if (result.action === TAKEDOWN && isStrongRef(result.subject)) { + await moderationTxn.takedownRecord({ + takedownId: result.id, + uri: new AtUri(result.subject.uri), + blobCids: result.subjectBlobCids.map((cid) => CID.parse(cid)), + }) + } + // apply label creation & negations + const applyLabels = (uri: string, cid: string | null) => + labelTxn.formatAndCreate(ctx.cfg.labelerDid, uri, cid, { + create: result.createLabelVals, + negate: result.negateLabelVals, + }) + if (isRepoRef(result.subject)) { + await applyLabels(result.subject.did, null) + } + if (isStrongRef(result.subject)) { + await applyLabels(result.subject.uri, result.subject.cid) + } + }) + + try { + await transact + } catch (err) { + req.log.error( + { err, actionId: result.id }, + 'proxied moderation action failed', + ) + } + + return { + encoding: 'application/json', + body: result, + } + } + const moderationService = services.moderation(db) const { action, diff --git a/packages/pds/src/api/com/atproto/admin/util.ts b/packages/pds/src/api/com/atproto/admin/util.ts new file mode 100644 index 00000000000..f8bab4460a5 --- /dev/null +++ b/packages/pds/src/api/com/atproto/admin/util.ts @@ -0,0 +1,41 @@ +import express from 'express' +import { + RepoView, + RepoViewDetail, +} from '../../../../lexicon/types/com/atproto/admin/defs' + +// Output designed to passed as second arg to AtpAgent methods. +// The encoding field here is a quirk of the AtpAgent. +export function authPassthru( + req: express.Request, + withEncoding?: false, +): { headers: { authorization: string }; encoding: undefined } | undefined + +export function authPassthru( + req: express.Request, + withEncoding: true, +): + | { headers: { authorization: string }; encoding: 'application/json' } + | undefined + +export function authPassthru(req: express.Request, withEncoding?: boolean) { + if (req.headers.authorization) { + return { + headers: { authorization: req.headers.authorization }, + encoding: withEncoding ? 'application/json' : undefined, + } + } +} + +// @NOTE mutates. +// merges-in details that the pds knows about the repo. +export function mergeRepoViewPdsDetails( + other: T, + pds: T, +) { + other.email ??= pds.email + other.invites ??= pds.invites + other.invitedBy ??= pds.invitedBy + other.invitesDisabled ??= pds.invitesDisabled + return other +} diff --git a/packages/pds/src/api/com/atproto/moderation/createReport.ts b/packages/pds/src/api/com/atproto/moderation/createReport.ts index af9f4fc3e9b..68aac3e86b3 100644 --- a/packages/pds/src/api/com/atproto/moderation/createReport.ts +++ b/packages/pds/src/api/com/atproto/moderation/createReport.ts @@ -6,9 +6,25 @@ export default function (server: Server, ctx: AppContext) { server.com.atproto.moderation.createReport({ auth: ctx.accessVerifierCheckTakedown, handler: async ({ input, auth }) => { + const requester = auth.credentials.did + + if (ctx.shouldProxyModeration()) { + const { data: result } = + await ctx.appviewAgent.com.atproto.moderation.createReport( + input.body, + { + ...(await ctx.serviceAuthHeaders(requester)), + encoding: 'application/json', + }, + ) + return { + encoding: 'application/json', + body: result, + } + } + const { db, services } = ctx const { reasonType, reason, subject } = input.body - const requester = auth.credentials.did const moderationService = services.moderation(db) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index 67322431164..6d7353c139c 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -4,6 +4,7 @@ import { FeedKeyset } from '../util/feed' import { paginate } from '../../../../../db/pagination' import AppContext from '../../../../../context' import { FeedRow } from '../../../../services/feed' +import { authPassthru } from '../../../../../api/com/atproto/admin/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getAuthorFeed({ @@ -16,12 +17,7 @@ export default function (server: Server, ctx: AppContext) { params, requester ? await ctx.serviceAuthHeaders(requester) - : { - // @TODO use authPassthru() once it lands - headers: req.headers.authorization - ? { authorization: req.headers.authorization } - : {}, - }, + : authPassthru(req), ) return { encoding: 'application/json', diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts index 2572702d78d..46407ec8b6d 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts @@ -1,7 +1,7 @@ import * as common from '@atproto/common' import { Server } from '../../../../../lexicon' import AppContext from '../../../../../context' -import { PostView } from '@atproto/api/src/client/types/app/bsky/feed/defs' +import { PostView } from '../../../../../lexicon/types/app/bsky/feed/defs' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getPosts({ diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index fdfb1e3677f..0d023008b0d 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -66,6 +66,7 @@ export interface ServerConfigValues { dbTxLockNonce?: string bskyAppViewEndpoint?: string + bskyAppViewModeration?: boolean bskyAppViewDid?: string bskyAppViewProxy: boolean @@ -211,6 +212,8 @@ export class ServerConfig { const bskyAppViewEndpoint = nonemptyString( process.env.BSKY_APP_VIEW_ENDPOINT, ) + const bskyAppViewModeration = + process.env.BSKY_APP_VIEW_MODERATION === 'true' ? true : false const bskyAppViewDid = nonemptyString(process.env.BSKY_APP_VIEW_DID) const bskyAppViewProxy = process.env.BSKY_APP_VIEW_PROXY === 'true' ? true : false @@ -268,6 +271,7 @@ export class ServerConfig { sequencerLeaderEnabled, dbTxLockNonce, bskyAppViewEndpoint, + bskyAppViewModeration, bskyAppViewDid, bskyAppViewProxy, crawlersToNotify, @@ -497,6 +501,10 @@ export class ServerConfig { return this.cfg.bskyAppViewEndpoint } + get bskyAppViewModeration() { + return this.cfg.bskyAppViewModeration + } + get bskyAppViewDid() { return this.cfg.bskyAppViewDid } diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 07ffdee5a1f..fb966b82e82 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -207,6 +207,13 @@ export class AppContext { ) } + shouldProxyModeration(): boolean { + return ( + this.cfg.bskyAppViewEndpoint !== undefined && + this.cfg.bskyAppViewModeration === true + ) + } + canProxyWrite(): boolean { return ( this.cfg.bskyAppViewEndpoint !== undefined && diff --git a/packages/pds/src/db/migrations/20230801T195109532Z-remove-moderation-fkeys.ts b/packages/pds/src/db/migrations/20230801T195109532Z-remove-moderation-fkeys.ts new file mode 100644 index 00000000000..81fd86d2b74 --- /dev/null +++ b/packages/pds/src/db/migrations/20230801T195109532Z-remove-moderation-fkeys.ts @@ -0,0 +1,56 @@ +import { Kysely } from 'kysely' +import { Dialect } from '..' + +export async function up(db: Kysely, dialect: Dialect): Promise { + if (dialect === 'sqlite') { + return + } + await db.schema + .alterTable('repo_root') + .dropConstraint('repo_root_takedown_id_fkey') + .execute() + await db.schema + .alterTable('record') + .dropConstraint('record_takedown_id_fkey') + .execute() + await db.schema + .alterTable('repo_blob') + .dropConstraint('repo_blob_takedown_id_fkey') + .execute() +} + +export async function down( + db: Kysely, + dialect: Dialect, +): Promise { + if (dialect === 'sqlite') { + return + } + await db.schema + .alterTable('repo_root') + .addForeignKeyConstraint( + 'repo_root_takedown_id_fkey', + ['takedownId'], + 'moderation_action', + ['id'], + ) + .execute() + await db.schema + .alterTable('record') + .addForeignKeyConstraint( + 'record_takedown_id_fkey', + ['takedownId'], + 'moderation_action', + ['id'], + ) + .execute() + await db.schema + .alterTable('repo_blob') + .addForeignKeyConstraint( + 'repo_blob_takedown_id_fkey', + ['takedownId'], + 'moderation_action', + ['id'], + ) + .execute() +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index f2f31275760..e64bae75ea2 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -57,3 +57,4 @@ export * as _20230703T044601833Z from './20230703T044601833Z-feed-and-label-indi export * as _20230718T170914772Z from './20230718T170914772Z-sequencer-leader-sequence' export * as _20230727T172043676Z from './20230727T172043676Z-user-account-cursor-idx' export * as _20230801T141349990Z from './20230801T141349990Z-invite-note' +export * as _20230801T195109532Z from './20230801T195109532Z-remove-moderation-fkeys' diff --git a/packages/pds/tests/labeler/apply-labels.test.ts b/packages/pds/tests/labeler/apply-labels.test.ts index d4a4255df13..756fd2c13fd 100644 --- a/packages/pds/tests/labeler/apply-labels.test.ts +++ b/packages/pds/tests/labeler/apply-labels.test.ts @@ -17,7 +17,7 @@ describe('unspecced.applyLabels', () => { beforeAll(async () => { server = await runTestServer({ - dbPostgresSchema: 'moderation', + dbPostgresSchema: 'apply_labels', }) close = server.close agent = new AtpAgent({ service: server.url }) diff --git a/packages/pds/tests/labeler/labeler.test.ts b/packages/pds/tests/labeler/labeler.test.ts index 1a3c5295359..646e5d621d9 100644 --- a/packages/pds/tests/labeler/labeler.test.ts +++ b/packages/pds/tests/labeler/labeler.test.ts @@ -26,7 +26,7 @@ describe('labeler', () => { beforeAll(async () => { server = await runTestServer({ - dbPostgresSchema: 'views_author_feed', + dbPostgresSchema: 'labeler', }) close = server.close ctx = server.ctx diff --git a/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap new file mode 100644 index 00000000000..ebdd162f7c1 --- /dev/null +++ b/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap @@ -0,0 +1,428 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`proxies admin requests creates reports of a repo. 1`] = ` +Array [ + Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "id": 1, + "reasonType": "com.atproto.moderation.defs#reasonSpam", + "reportedBy": "user(0)", + "subject": Object { + "$type": "com.atproto.admin.defs#repoRef", + "did": "user(1)", + }, + }, + Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "id": 2, + "reason": "impersonation", + "reasonType": "com.atproto.moderation.defs#reasonOther", + "reportedBy": "user(2)", + "subject": Object { + "$type": "com.atproto.admin.defs#repoRef", + "did": "user(1)", + }, + }, +] +`; + +exports[`proxies admin requests fetches a list of actions. 1`] = ` +Object { + "actions": Array [ + Object { + "action": "com.atproto.admin.defs#acknowledge", + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "did:example:admin", + "id": 3, + "reason": "Y", + "resolvedReportIds": Array [], + "reversal": Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "did:example:admin", + "reason": "X", + }, + "subject": Object { + "$type": "com.atproto.admin.defs#repoRef", + "did": "user(0)", + }, + "subjectBlobCids": Array [], + }, + Object { + "action": "com.atproto.admin.defs#flag", + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "did:example:admin", + "id": 2, + "reason": "Y", + "resolvedReportIds": Array [ + 2, + 1, + ], + "subject": Object { + "$type": "com.atproto.repo.strongRef", + "cid": "cids(0)", + "uri": "record(0)", + }, + "subjectBlobCids": Array [], + }, + ], + "cursor": "2", +} +`; + +exports[`proxies admin requests fetches a list of reports. 1`] = ` +Object { + "cursor": "2", + "reports": Array [ + Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "id": 1, + "reasonType": "com.atproto.moderation.defs#reasonSpam", + "reportedBy": "user(0)", + "resolvedByActionIds": Array [ + 2, + ], + "subject": Object { + "$type": "com.atproto.admin.defs#repoRef", + "did": "user(1)", + }, + "subjectRepoHandle": "bob.test", + }, + Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "id": 2, + "reason": "impersonation", + "reasonType": "com.atproto.moderation.defs#reasonOther", + "reportedBy": "user(2)", + "resolvedByActionIds": Array [ + 2, + ], + "subject": Object { + "$type": "com.atproto.admin.defs#repoRef", + "did": "user(1)", + }, + "subjectRepoHandle": "bob.test", + }, + ], +} +`; + +exports[`proxies admin requests fetches action details. 1`] = ` +Object { + "action": "com.atproto.admin.defs#acknowledge", + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "did:example:admin", + "id": 3, + "reason": "Y", + "resolvedReports": Array [], + "reversal": Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "did:example:admin", + "reason": "X", + }, + "subject": Object { + "$type": "com.atproto.admin.defs#repoView", + "did": "user(0)", + "handle": "bob.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "moderation": Object {}, + "relatedRecords": Array [ + Object { + "$type": "app.bsky.actor.profile", + "avatar": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(0)", + }, + "size": 3976, + }, + "description": "hi im bob label_me", + "displayName": "bobby", + }, + ], + }, + "subjectBlobs": Array [], +} +`; + +exports[`proxies admin requests fetches record details. 1`] = ` +Object { + "blobCids": Array [], + "blobs": Array [], + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "moderation": Object { + "actions": Array [ + Object { + "action": "com.atproto.admin.defs#flag", + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "did:example:admin", + "id": 2, + "reason": "Y", + "resolvedReportIds": Array [ + 2, + 1, + ], + "subject": Object { + "$type": "com.atproto.repo.strongRef", + "cid": "cids(0)", + "uri": "record(0)", + }, + "subjectBlobCids": Array [], + }, + ], + "currentAction": Object { + "action": "com.atproto.admin.defs#flag", + "id": 2, + }, + "reports": Array [], + }, + "repo": Object { + "did": "user(0)", + "email": "bob@test.com", + "handle": "bob.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "invitedBy": Object { + "available": 10, + "code": "invite-code", + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "admin", + "disabled": false, + "forAccount": "admin", + "uses": Array [ + Object { + "usedAt": "1970-01-01T00:00:00.000Z", + "usedBy": "user(1)", + }, + Object { + "usedAt": "1970-01-01T00:00:00.000Z", + "usedBy": "user(0)", + }, + Object { + "usedAt": "1970-01-01T00:00:00.000Z", + "usedBy": "user(2)", + }, + Object { + "usedAt": "1970-01-01T00:00:00.000Z", + "usedBy": "user(3)", + }, + ], + }, + "invitesDisabled": true, + "moderation": Object { + "currentAction": Object { + "action": "com.atproto.admin.defs#acknowledge", + "id": 3, + }, + }, + "relatedRecords": Array [ + Object { + "$type": "app.bsky.actor.profile", + "avatar": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(1)", + }, + "size": 3976, + }, + "description": "hi im bob label_me", + "displayName": "bobby", + }, + ], + }, + "uri": "record(0)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000+00:00", + "text": "bobby boy here", + }, +} +`; + +exports[`proxies admin requests fetches repo details. 1`] = ` +Object { + "did": "user(0)", + "email": "eve@test.com", + "handle": "eve.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "invitedBy": Object { + "available": 1, + "code": "invite-code", + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "admin", + "disabled": false, + "forAccount": "user(1)", + "uses": Array [ + Object { + "usedAt": "1970-01-01T00:00:00.000Z", + "usedBy": "user(0)", + }, + ], + }, + "invites": Array [], + "invitesDisabled": false, + "labels": Array [], + "moderation": Object { + "actions": Array [], + "reports": Array [], + }, + "relatedRecords": Array [], +} +`; + +exports[`proxies admin requests fetches report details. 1`] = ` +Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "id": 1, + "reasonType": "com.atproto.moderation.defs#reasonSpam", + "reportedBy": "user(0)", + "resolvedByActions": Array [ + Object { + "action": "com.atproto.admin.defs#flag", + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "did:example:admin", + "id": 2, + "reason": "Y", + "resolvedReportIds": Array [ + 2, + 1, + ], + "subject": Object { + "$type": "com.atproto.repo.strongRef", + "cid": "cids(1)", + "uri": "record(0)", + }, + "subjectBlobCids": Array [], + }, + ], + "subject": Object { + "$type": "com.atproto.admin.defs#repoView", + "did": "user(1)", + "handle": "bob.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "moderation": Object { + "currentAction": Object { + "action": "com.atproto.admin.defs#acknowledge", + "id": 3, + }, + }, + "relatedRecords": Array [ + Object { + "$type": "app.bsky.actor.profile", + "avatar": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(0)", + }, + "size": 3976, + }, + "description": "hi im bob label_me", + "displayName": "bobby", + }, + ], + }, +} +`; + +exports[`proxies admin requests reverses action. 1`] = ` +Object { + "action": "com.atproto.admin.defs#acknowledge", + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "did:example:admin", + "id": 3, + "reason": "Y", + "resolvedReportIds": Array [], + "reversal": Object { + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "did:example:admin", + "reason": "X", + }, + "subject": Object { + "$type": "com.atproto.admin.defs#repoRef", + "did": "user(0)", + }, + "subjectBlobCids": Array [], +} +`; + +exports[`proxies admin requests searches repos. 1`] = ` +Array [ + Object { + "did": "user(0)", + "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "moderation": Object {}, + "relatedRecords": Array [ + Object { + "$type": "app.bsky.actor.profile", + "avatar": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(0)", + }, + "size": 3976, + }, + "description": "its me!", + "displayName": "ali", + }, + ], + }, +] +`; + +exports[`proxies admin requests takes actions and resolves reports 1`] = ` +Object { + "action": "com.atproto.admin.defs#flag", + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "did:example:admin", + "id": 2, + "reason": "Y", + "resolvedReportIds": Array [], + "subject": Object { + "$type": "com.atproto.repo.strongRef", + "cid": "cids(0)", + "uri": "record(0)", + }, + "subjectBlobCids": Array [], +} +`; + +exports[`proxies admin requests takes actions and resolves reports 2`] = ` +Object { + "action": "com.atproto.admin.defs#acknowledge", + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "did:example:admin", + "id": 3, + "reason": "Y", + "resolvedReportIds": Array [], + "subject": Object { + "$type": "com.atproto.admin.defs#repoRef", + "did": "user(0)", + }, + "subjectBlobCids": Array [], +} +`; + +exports[`proxies admin requests takes actions and resolves reports 3`] = ` +Object { + "action": "com.atproto.admin.defs#flag", + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "did:example:admin", + "id": 2, + "reason": "Y", + "resolvedReportIds": Array [ + 2, + 1, + ], + "subject": Object { + "$type": "com.atproto.repo.strongRef", + "cid": "cids(0)", + "uri": "record(0)", + }, + "subjectBlobCids": Array [], +} +`; diff --git a/packages/pds/tests/proxied/admin.test.ts b/packages/pds/tests/proxied/admin.test.ts new file mode 100644 index 00000000000..d9c9b46b616 --- /dev/null +++ b/packages/pds/tests/proxied/admin.test.ts @@ -0,0 +1,405 @@ +import AtpAgent from '@atproto/api' +import { TestNetwork } from '@atproto/dev-env' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' +import { + REASONOTHER, + REASONSPAM, +} from '@atproto/api/src/client/types/com/atproto/moderation/defs' +import { forSnapshot } from '../_util' +import { + ACKNOWLEDGE, + FLAG, + TAKEDOWN, +} from '@atproto/api/src/client/types/com/atproto/admin/defs' +import { NotFoundError } from '@atproto/api/src/client/types/app/bsky/feed/getPostThread' + +describe('proxies admin requests', () => { + let network: TestNetwork + let agent: AtpAgent + let sc: SeedClient + + beforeAll(async () => { + 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. + enableInProcessAppView: true, + bskyAppViewModeration: true, + inviteRequired: true, + }, + }) + agent = network.pds.getClient() + sc = new SeedClient(agent) + const { data: invite } = + await agent.api.com.atproto.server.createInviteCode( + { useCount: 10 }, + { + encoding: 'application/json', + headers: network.pds.adminAuthHeaders(), + }, + ) + await basicSeed(sc, invite) + await network.processAll() + }) + + beforeAll(async () => { + const { data: invite } = + await agent.api.com.atproto.server.createInviteCode( + { useCount: 1, forAccount: sc.dids.alice }, + { + headers: network.pds.adminAuthHeaders(), + encoding: 'application/json', + }, + ) + await agent.api.com.atproto.admin.disableAccountInvites( + { account: sc.dids.bob }, + { headers: network.pds.adminAuthHeaders(), encoding: 'application/json' }, + ) + await sc.createAccount('eve', { + handle: 'eve.test', + email: 'eve@test.com', + password: 'password', + inviteCode: invite.code, + }) + await network.processAll() + }) + + afterAll(async () => { + await network.close() + }) + + it('creates reports of a repo.', async () => { + const { data: reportA } = + await agent.api.com.atproto.moderation.createReport( + { + reasonType: REASONSPAM, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, + }, + { + headers: sc.getHeaders(sc.dids.alice), + encoding: 'application/json', + }, + ) + const { data: reportB } = + await agent.api.com.atproto.moderation.createReport( + { + reasonType: REASONOTHER, + reason: 'impersonation', + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, + }, + { + headers: sc.getHeaders(sc.dids.carol), + encoding: 'application/json', + }, + ) + expect(forSnapshot([reportA, reportB])).toMatchSnapshot() + }) + + it('takes actions and resolves reports', async () => { + const post = sc.posts[sc.dids.bob][1] + const { data: actionA } = + await agent.api.com.atproto.admin.takeModerationAction( + { + action: FLAG, + subject: { + $type: 'com.atproto.repo.strongRef', + uri: post.ref.uriStr, + cid: post.ref.cidStr, + }, + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + headers: network.pds.adminAuthHeaders(), + encoding: 'application/json', + }, + ) + expect(forSnapshot(actionA)).toMatchSnapshot() + const { data: actionB } = + await agent.api.com.atproto.admin.takeModerationAction( + { + action: ACKNOWLEDGE, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + headers: network.pds.adminAuthHeaders(), + encoding: 'application/json', + }, + ) + expect(forSnapshot(actionB)).toMatchSnapshot() + const { data: resolved } = + await agent.api.com.atproto.admin.resolveModerationReports( + { + actionId: actionA.id, + reportIds: [1, 2], + createdBy: 'did:example:admin', + }, + { + headers: network.pds.adminAuthHeaders(), + encoding: 'application/json', + }, + ) + expect(forSnapshot(resolved)).toMatchSnapshot() + }) + + it('fetches report details.', async () => { + const { data: result } = + await agent.api.com.atproto.admin.getModerationReport( + { id: 1 }, + { headers: network.pds.adminAuthHeaders() }, + ) + expect(forSnapshot(result)).toMatchSnapshot() + }) + + it('fetches a list of reports.', async () => { + const { data: result } = + await agent.api.com.atproto.admin.getModerationReports( + { reverse: true }, + { headers: network.pds.adminAuthHeaders() }, + ) + expect(forSnapshot(result)).toMatchSnapshot() + }) + + it('fetches repo details.', async () => { + const { data: result } = await agent.api.com.atproto.admin.getRepo( + { did: sc.dids.eve }, + { headers: network.pds.adminAuthHeaders() }, + ) + expect(forSnapshot(result)).toMatchSnapshot() + }) + + it('fetches record details.', async () => { + const post = sc.posts[sc.dids.bob][1] + const { data: result } = await agent.api.com.atproto.admin.getRecord( + { uri: post.ref.uriStr }, + { headers: network.pds.adminAuthHeaders() }, + ) + expect(forSnapshot(result)).toMatchSnapshot() + }) + + it('reverses action.', async () => { + const { data: result } = + await agent.api.com.atproto.admin.reverseModerationAction( + { id: 3, createdBy: 'did:example:admin', reason: 'X' }, + { + headers: network.pds.adminAuthHeaders(), + encoding: 'application/json', + }, + ) + expect(forSnapshot(result)).toMatchSnapshot() + }) + + it('fetches action details.', async () => { + const { data: result } = + await agent.api.com.atproto.admin.getModerationAction( + { id: 3 }, + { headers: network.pds.adminAuthHeaders() }, + ) + expect(forSnapshot(result)).toMatchSnapshot() + }) + + it('fetches a list of actions.', async () => { + const { data: result } = + await agent.api.com.atproto.admin.getModerationActions( + { subject: sc.dids.bob }, + { headers: network.pds.adminAuthHeaders() }, + ) + expect(forSnapshot(result)).toMatchSnapshot() + }) + + it('searches repos.', async () => { + const { data: result } = await agent.api.com.atproto.admin.searchRepos( + { term: 'alice' }, + { headers: network.pds.adminAuthHeaders() }, + ) + expect(forSnapshot(result.repos)).toMatchSnapshot() + }) + + it('passes through errors.', async () => { + const tryGetReport = agent.api.com.atproto.admin.getModerationReport( + { id: 1000 }, + { headers: network.pds.adminAuthHeaders() }, + ) + await expect(tryGetReport).rejects.toThrow('Report not found') + const tryGetRepo = agent.api.com.atproto.admin.getRepo( + { did: 'did:does:not:exist' }, + { headers: network.pds.adminAuthHeaders() }, + ) + await expect(tryGetRepo).rejects.toThrow('Repo not found') + const tryGetRecord = agent.api.com.atproto.admin.getRecord( + { uri: 'at://did:does:not:exist/bad.collection.name/badrkey' }, + { headers: network.pds.adminAuthHeaders() }, + ) + await expect(tryGetRecord).rejects.toThrow('Record not found') + }) + + it('takesdown and labels repos, and reverts.', async () => { + const { db, services } = network.pds.ctx + // takedown repo + const { data: action } = + await agent.api.com.atproto.admin.takeModerationAction( + { + action: TAKEDOWN, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.alice, + }, + createdBy: 'did:example:admin', + reason: 'Y', + createLabelVals: ['dogs'], + negateLabelVals: ['cats'], + }, + { + headers: network.pds.adminAuthHeaders(), + encoding: 'application/json', + }, + ) + // check profile and labels + const tryGetProfilePds = agent.api.app.bsky.actor.getProfile( + { actor: sc.dids.alice }, + { headers: sc.getHeaders(sc.dids.carol) }, + ) + const tryGetProfileAppview = agent.api.app.bsky.actor.getProfile( + { actor: sc.dids.alice }, + { + headers: { ...sc.getHeaders(sc.dids.carol), 'x-appview-proxy': 'true' }, + }, + ) + await expect(tryGetProfilePds).rejects.toThrow( + 'Account has been taken down', + ) + await expect(tryGetProfileAppview).rejects.toThrow( + 'Account has been taken down', + ) + const labelsA = await services.appView + .label(db) + .getLabels(sc.dids.alice, false, true) + expect(labelsA.map((l) => l.val)).toEqual(['dogs']) + // reverse action + await agent.api.com.atproto.admin.reverseModerationAction( + { id: action.id, createdBy: 'did:example:admin', reason: 'X' }, + { + headers: network.pds.adminAuthHeaders(), + encoding: 'application/json', + }, + ) + // check profile and labels + const { data: profilePds } = await agent.api.app.bsky.actor.getProfile( + { actor: sc.dids.alice }, + { headers: sc.getHeaders(sc.dids.carol) }, + ) + const { data: profileAppview } = await agent.api.app.bsky.actor.getProfile( + { actor: sc.dids.alice }, + { + headers: { ...sc.getHeaders(sc.dids.carol), 'x-appview-proxy': 'true' }, + }, + ) + expect(profilePds).toEqual( + expect.objectContaining({ did: sc.dids.alice, handle: 'alice.test' }), + ) + expect(profileAppview).toEqual( + expect.objectContaining({ did: sc.dids.alice, handle: 'alice.test' }), + ) + const labelsB = await services.appView + .label(db) + .getLabels(sc.dids.alice, false, true) + expect(labelsB.map((l) => l.val)).toEqual(['cats']) + }) + + it('takesdown and labels records, and reverts.', async () => { + const { db, services } = network.pds.ctx + const post = sc.posts[sc.dids.alice][0] + // takedown post + const { data: action } = + await agent.api.com.atproto.admin.takeModerationAction( + { + action: TAKEDOWN, + subject: { + $type: 'com.atproto.repo.strongRef', + uri: post.ref.uriStr, + cid: post.ref.cidStr, + }, + createdBy: 'did:example:admin', + reason: 'Y', + createLabelVals: ['dogs'], + negateLabelVals: ['cats'], + }, + { + headers: network.pds.adminAuthHeaders(), + encoding: 'application/json', + }, + ) + // check thread and labels + const tryGetPostPds = agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr, depth: 0 }, + { headers: sc.getHeaders(sc.dids.carol) }, + ) + const tryGetPostAppview = agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr, depth: 0 }, + { + headers: { ...sc.getHeaders(sc.dids.carol), 'x-appview-proxy': 'true' }, + }, + ) + await expect(tryGetPostPds).rejects.toThrow(NotFoundError) + await expect(tryGetPostAppview).rejects.toThrow(NotFoundError) + const labelsA = await services.appView + .label(db) + .getLabels(post.ref.uriStr, false, true) + expect(labelsA.map((l) => l.val)).toEqual(['dogs']) + // reverse action + await agent.api.com.atproto.admin.reverseModerationAction( + { id: action.id, createdBy: 'did:example:admin', reason: 'X' }, + { + headers: network.pds.adminAuthHeaders(), + encoding: 'application/json', + }, + ) + // check thread and labels + const { data: threadPds } = await agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr, depth: 0 }, + { headers: sc.getHeaders(sc.dids.carol) }, + ) + const { data: threadAppview } = await agent.api.app.bsky.feed.getPostThread( + { uri: post.ref.uriStr, depth: 0 }, + { + headers: { ...sc.getHeaders(sc.dids.carol), 'x-appview-proxy': 'true' }, + }, + ) + expect(threadPds.thread.post).toEqual( + expect.objectContaining({ uri: post.ref.uriStr, cid: post.ref.cidStr }), + ) + expect(threadAppview.thread.post).toEqual( + expect.objectContaining({ uri: post.ref.uriStr, cid: post.ref.cidStr }), + ) + const labelsB = await services.appView + .label(db) + .getLabels(post.ref.uriStr, false, true) + expect(labelsB.map((l) => l.val)).toEqual(['cats']) + }) + + it('does not persist actions and reports on pds.', async () => { + const { db } = network.pds.ctx + const actions = await db.db + .selectFrom('moderation_action') + .selectAll() + .execute() + const reports = await db.db + .selectFrom('moderation_report') + .selectAll() + .execute() + expect(actions).toEqual([]) + expect(reports).toEqual([]) + }) +}) diff --git a/packages/pds/tests/seeds/basic.ts b/packages/pds/tests/seeds/basic.ts index 23b82219338..45de50f322e 100644 --- a/packages/pds/tests/seeds/basic.ts +++ b/packages/pds/tests/seeds/basic.ts @@ -4,8 +4,8 @@ import { adminAuth } from '../_util' import { SeedClient } from './client' import usersSeed from './users' -export default async (sc: SeedClient) => { - await usersSeed(sc) +export default async (sc: SeedClient, invite?: { code: string }) => { + await usersSeed(sc, invite) const alice = sc.dids.alice const bob = sc.dids.bob diff --git a/packages/pds/tests/seeds/users.ts b/packages/pds/tests/seeds/users.ts index 0a7d530d998..c94cd88817f 100644 --- a/packages/pds/tests/seeds/users.ts +++ b/packages/pds/tests/seeds/users.ts @@ -1,10 +1,10 @@ import { SeedClient } from './client' -export default async (sc: SeedClient) => { - await sc.createAccount('alice', users.alice) - await sc.createAccount('bob', users.bob) - await sc.createAccount('carol', users.carol) - await sc.createAccount('dan', users.dan) +export default async (sc: SeedClient, invite?: { code: string }) => { + await sc.createAccount('alice', { ...users.alice, inviteCode: invite?.code }) + await sc.createAccount('bob', { ...users.bob, inviteCode: invite?.code }) + await sc.createAccount('carol', { ...users.carol, inviteCode: invite?.code }) + await sc.createAccount('dan', { ...users.dan, inviteCode: invite?.code }) await sc.createProfile( sc.dids.alice, From 6321e7839f42b68cd548abcbd4e7b682fd21422b Mon Sep 17 00:00:00 2001 From: devin ivy Date: Mon, 7 Aug 2023 17:20:26 -0400 Subject: [PATCH 090/237] Include pds account info on mod report and action details (#1441) hydrate pds repo state onto mod report and action subject details --- .../com/atproto/admin/getModerationAction.ts | 34 ++++++++--- .../com/atproto/admin/getModerationReport.ts | 33 +++++++++-- .../proxied/__snapshots__/admin.test.ts.snap | 56 +++++++++++++++++++ 3 files changed, 110 insertions(+), 13 deletions(-) diff --git a/packages/pds/src/api/com/atproto/admin/getModerationAction.ts b/packages/pds/src/api/com/atproto/admin/getModerationAction.ts index 6005816999a..251979cbe54 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationAction.ts @@ -1,28 +1,48 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from './util' +import { authPassthru, mergeRepoViewPdsDetails } from './util' +import { isRepoView } from '@atproto/api/src/client/types/com/atproto/admin/defs' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationAction({ auth: ctx.roleVerifier, handler: async ({ req, params, auth }) => { + const access = auth.credentials + const { db, services } = ctx + const accountService = services.account(db) + const moderationService = services.moderation(db) + if (ctx.shouldProxyModeration()) { - // @TODO merge invite details into action subject - const { data: result } = + const { data: resultAppview } = await ctx.appviewAgent.com.atproto.admin.getModerationAction( params, authPassthru(req), ) + // merge local repo state for subject if available + if (isRepoView(resultAppview.subject)) { + const account = await accountService.getAccount( + resultAppview.subject.did, + true, + ) + const repo = + account && + (await moderationService.views.repo(account, { + includeEmails: access.moderator, + })) + if (repo) { + resultAppview.subject = mergeRepoViewPdsDetails( + resultAppview.subject, + repo, + ) + } + } return { encoding: 'application/json', - body: result, + body: resultAppview, } } - const access = auth.credentials - const { db, services } = ctx const { id } = params - const moderationService = services.moderation(db) const result = await moderationService.getActionOrThrow(id) return { encoding: 'application/json', diff --git a/packages/pds/src/api/com/atproto/admin/getModerationReport.ts b/packages/pds/src/api/com/atproto/admin/getModerationReport.ts index 0368a337c28..5af7381f780 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationReport.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationReport.ts @@ -1,27 +1,48 @@ +import { isRepoView } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { authPassthru } from './util' +import { authPassthru, mergeRepoViewPdsDetails } from './util' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationReport({ auth: ctx.roleVerifier, handler: async ({ req, params, auth }) => { + const access = auth.credentials + const { db, services } = ctx + const accountService = services.account(db) + const moderationService = services.moderation(db) + if (ctx.shouldProxyModeration()) { - const { data: result } = + const { data: resultAppview } = await ctx.appviewAgent.com.atproto.admin.getModerationReport( params, authPassthru(req), ) + // merge local repo state for subject if available + if (isRepoView(resultAppview.subject)) { + const account = await accountService.getAccount( + resultAppview.subject.did, + true, + ) + const repo = + account && + (await moderationService.views.repo(account, { + includeEmails: access.moderator, + })) + if (repo) { + resultAppview.subject = mergeRepoViewPdsDetails( + resultAppview.subject, + repo, + ) + } + } return { encoding: 'application/json', - body: result, + body: resultAppview, } } - const access = auth.credentials - const { db, services } = ctx const { id } = params - const moderationService = services.moderation(db) const result = await moderationService.getReportOrThrow(id) return { encoding: 'application/json', diff --git a/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap index ebdd162f7c1..46a88622b0f 100644 --- a/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap @@ -122,8 +122,36 @@ Object { "subject": Object { "$type": "com.atproto.admin.defs#repoView", "did": "user(0)", + "email": "bob@test.com", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", + "invitedBy": Object { + "available": 10, + "code": "invite-code", + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "admin", + "disabled": false, + "forAccount": "admin", + "uses": Array [ + Object { + "usedAt": "1970-01-01T00:00:00.000Z", + "usedBy": "user(1)", + }, + Object { + "usedAt": "1970-01-01T00:00:00.000Z", + "usedBy": "user(0)", + }, + Object { + "usedAt": "1970-01-01T00:00:00.000Z", + "usedBy": "user(2)", + }, + Object { + "usedAt": "1970-01-01T00:00:00.000Z", + "usedBy": "user(3)", + }, + ], + }, + "invitesDisabled": true, "moderation": Object {}, "relatedRecords": Array [ Object { @@ -300,8 +328,36 @@ Object { "subject": Object { "$type": "com.atproto.admin.defs#repoView", "did": "user(1)", + "email": "bob@test.com", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", + "invitedBy": Object { + "available": 10, + "code": "invite-code", + "createdAt": "1970-01-01T00:00:00.000Z", + "createdBy": "admin", + "disabled": false, + "forAccount": "admin", + "uses": Array [ + Object { + "usedAt": "1970-01-01T00:00:00.000Z", + "usedBy": "user(0)", + }, + Object { + "usedAt": "1970-01-01T00:00:00.000Z", + "usedBy": "user(1)", + }, + Object { + "usedAt": "1970-01-01T00:00:00.000Z", + "usedBy": "user(2)", + }, + Object { + "usedAt": "1970-01-01T00:00:00.000Z", + "usedBy": "user(3)", + }, + ], + }, + "invitesDisabled": true, "moderation": Object { "currentAction": Object { "action": "com.atproto.admin.defs#acknowledge", From b5569121f89fe553eb857d5a31faad70235f5ddb Mon Sep 17 00:00:00 2001 From: devin ivy Date: Mon, 7 Aug 2023 19:38:04 -0400 Subject: [PATCH 091/237] Misc perf improvements on actor search, post deletion, actor invite codes (#1436) * tighten fuzzy actor search for larger dataset * add indexes to support post deletion on feed_item, listing user invites on invite_code * fix migration whitespace * temp comment migrations and build * Revert "temp comment migrations and build" This reverts commit 225d2c00cce1885f2d2cffc4724843eda4b01cee. --- packages/bsky/src/db/index.ts | 2 +- ...1Z-feed-item-delete-invite-for-user-idx.ts | 14 ++++ packages/bsky/src/db/migrations/index.ts | 1 + packages/bsky/src/services/util/search.ts | 16 +--- .../bsky/tests/views/actor-search.test.ts | 20 +++-- packages/pds/src/db/index.ts | 2 +- ...1Z-feed-item-delete-invite-for-user-idx.ts | 21 ++++++ packages/pds/src/db/migrations/index.ts | 1 + packages/pds/src/services/util/search.ts | 16 +--- packages/pds/tests/views/actor-search.test.ts | 16 +++- .../pds/tests/views/admin/repo-search.test.ts | 74 +++++++++---------- 11 files changed, 113 insertions(+), 70 deletions(-) create mode 100644 packages/bsky/src/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts create mode 100644 packages/pds/src/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts diff --git a/packages/bsky/src/db/index.ts b/packages/bsky/src/db/index.ts index e31ee5ffea2..8ca09b0f20d 100644 --- a/packages/bsky/src/db/index.ts +++ b/packages/bsky/src/db/index.ts @@ -54,7 +54,7 @@ export class Database { pool.on('connect', (client) => { // Used for trigram indexes, e.g. on actor search - client.query('SET pg_trgm.strict_word_similarity_threshold TO .1;') + 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;`) diff --git a/packages/bsky/src/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts b/packages/bsky/src/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts new file mode 100644 index 00000000000..2e1c05ba33a --- /dev/null +++ b/packages/bsky/src/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts @@ -0,0 +1,14 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + // supports post deletion + await db.schema + .createIndex('feed_item_post_uri_idx') + .on('feed_item') + .column('postUri') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropIndex('feed_item_post_uri_idx').execute() +} diff --git a/packages/bsky/src/db/migrations/index.ts b/packages/bsky/src/db/migrations/index.ts index daa87e0e1d8..dd4cfb3c641 100644 --- a/packages/bsky/src/db/migrations/index.ts +++ b/packages/bsky/src/db/migrations/index.ts @@ -21,3 +21,4 @@ export * as _20230627T212437895Z from './20230627T212437895Z-optional-handle' export * as _20230629T220835893Z from './20230629T220835893Z-remove-post-hierarchy' export * as _20230703T045536691Z from './20230703T045536691Z-feed-and-label-indices' export * as _20230720T164800037Z from './20230720T164800037Z-posts-cursor-idx' +export * as _20230807T035309811Z from './20230807T035309811Z-feed-item-delete-invite-for-user-idx' diff --git a/packages/bsky/src/services/util/search.ts b/packages/bsky/src/services/util/search.ts index f32ce468f12..e5b4b6b341e 100644 --- a/packages/bsky/src/services/util/search.ts +++ b/packages/bsky/src/services/util/search.ts @@ -83,7 +83,6 @@ const getMatchingAccountsQb = ( ) .where('actor.handle', 'is not', null) .where(similar(term, ref('handle'))) // Coarse filter engaging trigram index - .where(distanceAccount, '<', getMatchThreshold(term)) // Refines results from trigram index .select(['actor.did as did', distanceAccount.as('distance')]) } @@ -103,7 +102,6 @@ const getMatchingProfilesQb = ( ) .where('actor.handle', 'is not', null) .where(similar(term, ref('displayName'))) // Coarse filter engaging trigram index - .where(distanceProfile, '<', getMatchThreshold(term)) // Refines results from trigram index .select(['profile.creator as did', distanceProfile.as('distance')]) } @@ -139,17 +137,11 @@ export const cleanTerm = (term: string) => term.trim().replace(/^@/g, '') // Uses pg_trgm strict word similarity to check similarity between a search term and a stored value const distance = (term: string, ref: DbRef) => - sql`(${term} <<<-> ${ref})` + sql`(${term} <<-> ${ref})` -// Can utilize trigram index to match on strict word similarity -const similar = (term: string, ref: DbRef) => sql`(${term} <<% ${ref})` - -const getMatchThreshold = (term: string) => { - // Performing matching by word using "strict word similarity" operator. - // The more characters the user gives us, the more we can ratchet down - // the distance threshold for matching. - return term.length < 3 ? 0.9 : 0.8 -} +// Can utilize trigram index to match on strict word similarity. +// The word_similarity_threshold is set to .4 (i.e. distance < .6) in db/index.ts. +const similar = (term: string, ref: DbRef) => sql`(${term} <% ${ref})` type Result = { distance: number; did: string } type LabeledResult = { primary: number; secondary: string } diff --git a/packages/bsky/tests/views/actor-search.test.ts b/packages/bsky/tests/views/actor-search.test.ts index 59c21587a19..b3e4fc15ffb 100644 --- a/packages/bsky/tests/views/actor-search.test.ts +++ b/packages/bsky/tests/views/actor-search.test.ts @@ -68,8 +68,7 @@ describe('pds actor search views', () => { ] shouldContain.forEach((handle) => expect(handles).toContain(handle)) - - expect(handles).toContain('cayla-marquardt39.test') // Fuzzy match supported by postgres + expect(handles).toContain('cayla-marquardt39.test') // Fuzzy match const shouldNotContain = [ 'sven70.test', @@ -111,7 +110,19 @@ describe('pds actor search views', () => { { headers }, ) - expect(limited.data.actors).toEqual(full.data.actors.slice(0, 5)) + // @NOTE it's expected that searchActorsTypeahead doesn't have stable pagination + + const limitedIndexInFull = limited.data.actors.map((needle) => { + return full.data.actors.findIndex( + (haystack) => needle.did === haystack.did, + ) + }) + + // subset exists in full and is monotonic + expect(limitedIndexInFull.every((idx) => idx !== -1)).toEqual(true) + expect(limitedIndexInFull).toEqual( + [...limitedIndexInFull].sort((a, b) => a - b), + ) }) it('typeahead gives results unauthed', async () => { @@ -146,8 +157,7 @@ describe('pds actor search views', () => { ] shouldContain.forEach((handle) => expect(handles).toContain(handle)) - - expect(handles).toContain('cayla-marquardt39.test') // Fuzzy match supported by postgres + expect(handles).toContain('cayla-marquardt39.test') // Fuzzy match const shouldNotContain = [ 'sven70.test', diff --git a/packages/pds/src/db/index.ts b/packages/pds/src/db/index.ts index 52941047a2c..b98706e7bd9 100644 --- a/packages/pds/src/db/index.ts +++ b/packages/pds/src/db/index.ts @@ -81,7 +81,7 @@ export class Database { pool.on('connect', (client) => { // Used for trigram indexes, e.g. on actor search - client.query('SET pg_trgm.strict_word_similarity_threshold TO .1;') + 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;`) diff --git a/packages/pds/src/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts b/packages/pds/src/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts new file mode 100644 index 00000000000..1cad46fcfb4 --- /dev/null +++ b/packages/pds/src/db/migrations/20230807T035309811Z-feed-item-delete-invite-for-user-idx.ts @@ -0,0 +1,21 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + // supports post deletion + await db.schema + .createIndex('feed_item_post_uri_idx') + .on('feed_item') + .column('postUri') + .execute() + // supports listing user invites + await db.schema + .createIndex('invite_code_for_user_idx') + .on('invite_code') + .column('forUser') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropIndex('feed_item_post_uri_idx').execute() + await db.schema.dropIndex('invite_code_for_user_idx').execute() +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index e64bae75ea2..32a5bfa0f11 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -58,3 +58,4 @@ export * as _20230718T170914772Z from './20230718T170914772Z-sequencer-leader-se export * as _20230727T172043676Z from './20230727T172043676Z-user-account-cursor-idx' export * as _20230801T141349990Z from './20230801T141349990Z-invite-note' export * as _20230801T195109532Z from './20230801T195109532Z-remove-moderation-fkeys' +export * as _20230807T035309811Z from './20230807T035309811Z-feed-item-delete-invite-for-user-idx' diff --git a/packages/pds/src/services/util/search.ts b/packages/pds/src/services/util/search.ts index fc735f14d9d..007052e707c 100644 --- a/packages/pds/src/services/util/search.ts +++ b/packages/pds/src/services/util/search.ts @@ -88,7 +88,6 @@ const getMatchingAccountsQb = ( qb.where(notSoftDeletedClause(ref('repo_root'))), ) .where(similar(term, ref('handle'))) // Coarse filter engaging trigram index - .where(distanceAccount, '<', getMatchThreshold(term)) // Refines results from trigram index .select(['did_handle.did as did', distanceAccount.as('distance')]) } @@ -107,7 +106,6 @@ const getMatchingProfilesQb = ( qb.where(notSoftDeletedClause(ref('repo_root'))), ) .where(similar(term, ref('displayName'))) // Coarse filter engaging trigram index - .where(distanceProfile, '<', getMatchThreshold(term)) // Refines results from trigram index .select(['profile.creator as did', distanceProfile.as('distance')]) } @@ -199,17 +197,11 @@ export const cleanTerm = (term: string) => term.trim().replace(/^@/g, '') // Uses pg_trgm strict word similarity to check similarity between a search term and a stored value const distance = (term: string, ref: DbRef) => - sql`(${term} <<<-> ${ref})` + sql`(${term} <<-> ${ref})` -// Can utilize trigram index to match on strict word similarity -const similar = (term: string, ref: DbRef) => sql`(${term} <<% ${ref})` - -const getMatchThreshold = (term: string) => { - // Performing matching by word using "strict word similarity" operator. - // The more characters the user gives us, the more we can ratchet down - // the distance threshold for matching. - return term.length < 3 ? 0.9 : 0.8 -} +// Can utilize trigram index to match on strict word similarity. +// The word_similarity_threshold is set to .4 (i.e. distance < .6) in db/index.ts. +const similar = (term: string, ref: DbRef) => sql`(${term} <% ${ref})` type Result = { distance: number; handle: string } type LabeledResult = { primary: number; secondary: string } diff --git a/packages/pds/tests/views/actor-search.test.ts b/packages/pds/tests/views/actor-search.test.ts index f778f1387c4..d110d8e2e68 100644 --- a/packages/pds/tests/views/actor-search.test.ts +++ b/packages/pds/tests/views/actor-search.test.ts @@ -104,7 +104,19 @@ describe('pds user search views', () => { { headers }, ) - expect(limited.data.actors).toEqual(full.data.actors.slice(0, 5)) + // @NOTE it's expected that searchActorsTypeahead doesn't have stable pagination + + const limitedIndexInFull = limited.data.actors.map((needle) => { + return full.data.actors.findIndex( + (haystack) => needle.did === haystack.did, + ) + }) + + // subset exists in full and is monotonic + expect(limitedIndexInFull.every((idx) => idx !== -1)).toEqual(true) + expect(limitedIndexInFull).toEqual( + [...limitedIndexInFull].sort((a, b) => a - b), + ) }) it('search gives relevant results', async () => { @@ -118,7 +130,7 @@ describe('pds user search views', () => { const shouldContain = [ 'cara-wiegand69.test', 'eudora-dietrich4.test', // Carol Littel - 'shane-torphy52.test', //Sadie Carter + 'shane-torphy52.test', // Sadie Carter 'aliya-hodkiewicz.test', // Carlton Abernathy IV 'carlos6.test', 'carolina-mcdermott77.test', diff --git a/packages/pds/tests/views/admin/repo-search.test.ts b/packages/pds/tests/views/admin/repo-search.test.ts index 2da1f328247..e468a58a663 100644 --- a/packages/pds/tests/views/admin/repo-search.test.ts +++ b/packages/pds/tests/views/admin/repo-search.test.ts @@ -55,7 +55,7 @@ describe('pds admin repo search view', () => { const shouldContain = [ 'cara-wiegand69.test', // Present despite repo takedown 'eudora-dietrich4.test', // Carol Littel - 'shane-torphy52.test', //Sadie Carter + 'shane-torphy52.test', // Sadie Carter 'aliya-hodkiewicz.test', // Carlton Abernathy IV 'carlos6.test', 'carolina-mcdermott77.test', @@ -156,22 +156,8 @@ const snapPg = ` Array [ Object { "did": "user(0)", - "email": "cara-wiegand69.test@bsky.app", - "handle": "cara-wiegand69.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object { - "currentAction": Object { - "action": "com.atproto.admin.defs#takedown", - "id": 1, - }, - }, - "relatedRecords": Array [], - }, - Object { - "did": "user(1)", - "email": "eudora-dietrich4.test@bsky.app", - "handle": "eudora-dietrich4.test", + "email": "aliya-hodkiewicz.test@bsky.app", + "handle": "aliya-hodkiewicz.test", "indexedAt": "1970-01-01T00:00:00.000Z", "invitesDisabled": false, "moderation": Object {}, @@ -187,14 +173,37 @@ Array [ "size": 3976, }, "description": "", - "displayName": "Carol Littel", + "displayName": "Carlton Abernathy IV", }, ], }, + Object { + "did": "user(1)", + "email": "cara-wiegand69.test@bsky.app", + "handle": "cara-wiegand69.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "invitesDisabled": false, + "moderation": Object { + "currentAction": Object { + "action": "com.atproto.admin.defs#takedown", + "id": 1, + }, + }, + "relatedRecords": Array [], + }, Object { "did": "user(2)", - "email": "shane-torphy52.test@bsky.app", - "handle": "shane-torphy52.test", + "email": "carlos6.test@bsky.app", + "handle": "carlos6.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "invitesDisabled": false, + "moderation": Object {}, + "relatedRecords": Array [], + }, + Object { + "did": "user(3)", + "email": "carolina-mcdermott77.test@bsky.app", + "handle": "carolina-mcdermott77.test", "indexedAt": "1970-01-01T00:00:00.000Z", "invitesDisabled": false, "moderation": Object {}, @@ -210,14 +219,14 @@ Array [ "size": 3976, }, "description": "", - "displayName": "Sadie Carter", + "displayName": "Latoya Windler", }, ], }, Object { - "did": "user(3)", - "email": "aliya-hodkiewicz.test@bsky.app", - "handle": "aliya-hodkiewicz.test", + "did": "user(4)", + "email": "eudora-dietrich4.test@bsky.app", + "handle": "eudora-dietrich4.test", "indexedAt": "1970-01-01T00:00:00.000Z", "invitesDisabled": false, "moderation": Object {}, @@ -233,23 +242,14 @@ Array [ "size": 3976, }, "description": "", - "displayName": "Carlton Abernathy IV", + "displayName": "Carol Littel", }, ], }, - Object { - "did": "user(4)", - "email": "carlos6.test@bsky.app", - "handle": "carlos6.test", - "indexedAt": "1970-01-01T00:00:00.000Z", - "invitesDisabled": false, - "moderation": Object {}, - "relatedRecords": Array [], - }, Object { "did": "user(5)", - "email": "carolina-mcdermott77.test@bsky.app", - "handle": "carolina-mcdermott77.test", + "email": "shane-torphy52.test@bsky.app", + "handle": "shane-torphy52.test", "indexedAt": "1970-01-01T00:00:00.000Z", "invitesDisabled": false, "moderation": Object {}, @@ -265,7 +265,7 @@ Array [ "size": 3976, }, "description": "", - "displayName": "Latoya Windler", + "displayName": "Sadie Carter", }, ], }, From 556e438ccc94d09c5d366f5c918edb242a989dd9 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 7 Aug 2023 18:52:10 -0500 Subject: [PATCH 092/237] Filter out blocked posts on getPosts (#1442) filter out blocked posts on getPosts --- .../bsky/src/api/app/bsky/feed/getPosts.ts | 5 ++++- packages/bsky/tests/views/blocks.test.ts | 22 +++++++++++++++++++ .../app-view/api/app/bsky/feed/getPosts.ts | 6 ++++- packages/pds/tests/views/blocks.test.ts | 22 +++++++++++++++++++ 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getPosts.ts b/packages/bsky/src/api/app/bsky/feed/getPosts.ts index 615cff616ed..5df013d30a3 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPosts.ts @@ -18,7 +18,10 @@ export default function (server: Server, ctx: AppContext) { const posts: PostView[] = [] for (const uri of uris) { const post = postViews[uri] - if (post) { + const isBlocked = + post.author.viewer?.blockedBy === true || + typeof post.author.viewer?.blocking === 'string' + if (post && !isBlocked) { posts.push(post) } } diff --git a/packages/bsky/tests/views/blocks.test.ts b/packages/bsky/tests/views/blocks.test.ts index 230a92de12e..691487f9fc7 100644 --- a/packages/bsky/tests/views/blocks.test.ts +++ b/packages/bsky/tests/views/blocks.test.ts @@ -212,6 +212,28 @@ describe('pds views with blocking', () => { expect(resDan.data.followers.some((f) => f.did === carol)).toBe(false) }) + it('does not return posts from blocked users', async () => { + const alicePost = sc.posts[alice][0].ref.uriStr + const carolPost = sc.posts[carol][0].ref.uriStr + const danPost = sc.posts[dan][0].ref.uriStr + + const resCarol = await agent.api.app.bsky.feed.getPosts( + { uris: [alicePost, carolPost, danPost] }, + { headers: await network.serviceHeaders(carol) }, + ) + expect(resCarol.data.posts.some((p) => p.uri === alicePost)).toBe(true) + expect(resCarol.data.posts.some((p) => p.uri === carolPost)).toBe(true) + expect(resCarol.data.posts.some((p) => p.uri === danPost)).toBe(false) + + const resDan = await agent.api.app.bsky.feed.getPosts( + { uris: [alicePost, carolPost, danPost] }, + { headers: await network.serviceHeaders(dan) }, + ) + expect(resDan.data.posts.some((p) => p.uri === alicePost)).toBe(true) + expect(resDan.data.posts.some((p) => p.uri === carolPost)).toBe(false) + expect(resDan.data.posts.some((p) => p.uri === danPost)).toBe(true) + }) + it('does not return notifs for blocked accounts', async () => { const resCarol = await agent.api.app.bsky.notification.listNotifications( { diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts index 46407ec8b6d..1e180220eba 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts @@ -28,7 +28,11 @@ export default function (server: Server, ctx: AppContext) { const posts: PostView[] = [] for (const uri of uris) { const post = postViews[uri] - if (post) { + const isBlocked = + post.author.viewer?.blockedBy === true || + typeof post.author.viewer?.blocking === 'string' + + if (post && !isBlocked) { posts.push(post) } } diff --git a/packages/pds/tests/views/blocks.test.ts b/packages/pds/tests/views/blocks.test.ts index 518c06181f6..b56f80cd18e 100644 --- a/packages/pds/tests/views/blocks.test.ts +++ b/packages/pds/tests/views/blocks.test.ts @@ -254,6 +254,28 @@ describe('pds views with blocking', () => { expect(resDan.data.followers.some((f) => f.did === carol)).toBe(false) }) + it('does not return posts from blocked users', async () => { + const alicePost = sc.posts[alice][0].ref.uriStr + const carolPost = sc.posts[carol][0].ref.uriStr + const danPost = sc.posts[dan][0].ref.uriStr + + const resCarol = await agent.api.app.bsky.feed.getPosts( + { uris: [alicePost, carolPost, danPost] }, + { headers: sc.getHeaders(carol) }, + ) + expect(resCarol.data.posts.some((p) => p.uri === alicePost)).toBe(true) + expect(resCarol.data.posts.some((p) => p.uri === carolPost)).toBe(true) + expect(resCarol.data.posts.some((p) => p.uri === danPost)).toBe(false) + + const resDan = await agent.api.app.bsky.feed.getPosts( + { uris: [alicePost, carolPost, danPost] }, + { headers: sc.getHeaders(dan) }, + ) + expect(resDan.data.posts.some((p) => p.uri === alicePost)).toBe(true) + expect(resDan.data.posts.some((p) => p.uri === carolPost)).toBe(false) + expect(resDan.data.posts.some((p) => p.uri === danPost)).toBe(true) + }) + it('does not return notifs for blocked accounts', async () => { const resCarol = await agent.api.app.bsky.notification.listNotifications( { From bb4b4344bd7c8c6a566dd0f0100739463806544e Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 7 Aug 2023 18:54:33 -0500 Subject: [PATCH 093/237] Get post hotfix (#1445) ensure post is defined --- packages/bsky/src/api/app/bsky/feed/getPosts.ts | 4 ++-- packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getPosts.ts b/packages/bsky/src/api/app/bsky/feed/getPosts.ts index 5df013d30a3..d964e3c5c57 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPosts.ts @@ -19,8 +19,8 @@ export default function (server: Server, ctx: AppContext) { for (const uri of uris) { const post = postViews[uri] const isBlocked = - post.author.viewer?.blockedBy === true || - typeof post.author.viewer?.blocking === 'string' + post?.author.viewer?.blockedBy === true || + typeof post?.author.viewer?.blocking === 'string' if (post && !isBlocked) { posts.push(post) } diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts index 1e180220eba..48e764862c0 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts @@ -29,8 +29,8 @@ export default function (server: Server, ctx: AppContext) { for (const uri of uris) { const post = postViews[uri] const isBlocked = - post.author.viewer?.blockedBy === true || - typeof post.author.viewer?.blocking === 'string' + post?.author.viewer?.blockedBy === true || + typeof post?.author.viewer?.blocking === 'string' if (post && !isBlocked) { posts.push(post) From 5cc57d5ebce5af8c3082897b9ea83acabd395b3b Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 7 Aug 2023 19:26:07 -0500 Subject: [PATCH 094/237] Hotfix: remove relative imports from mod routes (#1446) remove relative imports from mod methods --- packages/pds/src/api/com/atproto/admin/getModerationAction.ts | 2 +- packages/pds/src/api/com/atproto/admin/getModerationReport.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pds/src/api/com/atproto/admin/getModerationAction.ts b/packages/pds/src/api/com/atproto/admin/getModerationAction.ts index 251979cbe54..10d38174149 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationAction.ts @@ -1,7 +1,7 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { authPassthru, mergeRepoViewPdsDetails } from './util' -import { isRepoView } from '@atproto/api/src/client/types/com/atproto/admin/defs' +import { isRepoView } from '../../../../lexicon/types/com/atproto/admin/defs' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationAction({ diff --git a/packages/pds/src/api/com/atproto/admin/getModerationReport.ts b/packages/pds/src/api/com/atproto/admin/getModerationReport.ts index 5af7381f780..7c0592177fa 100644 --- a/packages/pds/src/api/com/atproto/admin/getModerationReport.ts +++ b/packages/pds/src/api/com/atproto/admin/getModerationReport.ts @@ -1,7 +1,7 @@ -import { isRepoView } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { authPassthru, mergeRepoViewPdsDetails } from './util' +import { isRepoView } from '../../../../lexicon/types/com/atproto/admin/defs' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationReport({ From b16c41c2fa24a18eb6d93e01b2a930f22d237198 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 8 Aug 2023 10:41:19 -0500 Subject: [PATCH 095/237] Tighten feed filtering (#1443) tighten feed filtering --- .../bsky/src/api/app/bsky/feed/getFeed.ts | 66 +-------- .../bsky/src/api/app/bsky/feed/getTimeline.ts | 133 ++++++++++++------ .../app/bsky/unspecced/getTimelineSkeleton.ts | 80 +---------- packages/bsky/src/services/feed/index.ts | 58 +++++++- 4 files changed, 152 insertions(+), 185 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getFeed.ts b/packages/bsky/src/api/app/bsky/feed/getFeed.ts index 6602e937a58..ffed4a0e5a7 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeed.ts @@ -11,12 +11,10 @@ import { getFeedGen, } from '@atproto/identity' import { AtpAgent, AppBskyFeedGetFeedSkeleton } from '@atproto/api' -import { SkeletonFeedPost } from '../../../../lexicon/types/app/bsky/feed/defs' import { QueryParams as GetFeedParams } from '../../../../lexicon/types/app/bsky/feed/getFeed' import { OutputSchema as SkeletonOutput } from '../../../../lexicon/types/app/bsky/feed/getFeedSkeleton' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { FeedRow } from '../../../../services/feed/types' import { AlgoResponse } from '../../../../feed-gen/types' export default function (server: Server, ctx: AppContext) { @@ -127,68 +125,12 @@ async function skeletonFromFeedGen( } const { feed: skeletonFeed, ...rest } = skeleton + const cleanedFeed = await ctx.services + .feed(ctx.db) + .cleanFeedSkeleton(skeletonFeed, params.limit, viewer) - // Hydrate feed skeleton - const { ref } = ctx.db.db.dynamic - const feedService = ctx.services.feed(ctx.db) - const graphService = ctx.services.graph(ctx.db) - const feedItemUris = skeletonFeed.map(getSkeleFeedItemUri) - - // @TODO apply mutes and blocks - const feedItems = feedItemUris.length - ? await feedService - .selectFeedItemQb() - .where('feed_item.uri', 'in', feedItemUris) - .where((qb) => - // Hide posts and reposts of or by muted actors - graphService.whereNotMuted(qb, viewer, [ - ref('post.creator'), - ref('originatorDid'), - ]), - ) - .whereNotExists( - graphService.blockQb(viewer, [ - ref('post.creator'), - ref('originatorDid'), - ]), - ) - .execute() - : [] - - const orderedItems = getOrderedFeedItems(skeletonFeed, feedItems, params) return { ...rest, - feedItems: orderedItems, - } -} - -function getSkeleFeedItemUri(item: SkeletonFeedPost) { - if (typeof item.reason?.repost === 'string') { - return item.reason.repost + feedItems: cleanedFeed, } - return item.post -} - -function getOrderedFeedItems( - skeletonItems: SkeletonFeedPost[], - feedItems: FeedRow[], - params: GetFeedParams, -) { - const SKIP = [] - const feedItemsByUri = feedItems.reduce((acc, item) => { - return Object.assign(acc, { [item.uri]: item }) - }, {} as Record) - // enforce limit param in the case that the feedgen does not - if (skeletonItems.length > params.limit) { - skeletonItems = skeletonItems.slice(0, params.limit) - } - return skeletonItems.flatMap((item) => { - const uri = getSkeleFeedItemUri(item) - const feedItem = feedItemsByUri[uri] - if (!feedItem || item.post !== feedItem.postUri) { - // Couldn't find the record, or skeleton repost referenced the wrong post - return SKIP - } - return feedItem - }) } diff --git a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts index 142283aacf3..80598220ffb 100644 --- a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts +++ b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts @@ -3,73 +3,116 @@ import { Server } from '../../../../lexicon' import { FeedAlgorithm, FeedKeyset, getFeedDateThreshold } from '../util/feed' import { paginate } from '../../../../db/pagination' import AppContext from '../../../../context' +import Database from '../../../../db' +import { SkeletonFeedPost } from '../../../../lexicon/types/app/bsky/feed/defs' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getTimeline({ auth: ctx.authVerifier, handler: async ({ params, auth }) => { const { algorithm, limit, cursor } = params - const db = ctx.db.db - const { ref } = db.dynamic const viewer = auth.credentials.did if (algorithm && algorithm !== FeedAlgorithm.ReverseChronological) { throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`) } - const feedService = ctx.services.feed(ctx.db) - const graphService = ctx.services.graph(ctx.db) - - const followingIdsSubquery = db - .selectFrom('follow') - .select('follow.subjectDid') - .where('follow.creator', '=', viewer) - - const keyset = new FeedKeyset( - ref('feed_item.sortAt'), - ref('feed_item.cid'), - ) - const sortFrom = keyset.unpack(cursor)?.primary - - let feedItemsQb = feedService - .selectFeedItemQb() - .where((qb) => - qb - .where('originatorDid', '=', viewer) - .orWhere('originatorDid', 'in', followingIdsSubquery), - ) - .where((qb) => - // Hide posts and reposts of or by muted actors - graphService.whereNotMuted(qb, viewer, [ - ref('post.creator'), - ref('originatorDid'), - ]), - ) - .whereNotExists( - graphService.blockQb(viewer, [ - ref('post.creator'), - ref('originatorDid'), - ]), - ) - .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom)) + const skeleton = await getTimelineSkeleton(ctx.db, viewer, limit, cursor) - feedItemsQb = paginate(feedItemsQb, { + const feedService = ctx.services.feed(ctx.db) + const feedItems = await feedService.cleanFeedSkeleton( + skeleton.feed, limit, - cursor, - keyset, - tryIndex: true, - }) - - const feedItems = await feedItemsQb.execute() + viewer, + ) const feed = await feedService.hydrateFeed(feedItems, viewer) return { encoding: 'application/json', body: { feed, - cursor: keyset.packFromResult(feedItems), + cursor: skeleton.cursor, }, } }, }) } + +export const getTimelineSkeleton = async ( + db: Database, + viewer: string, + limit: number, + cursor?: string, +): Promise<{ feed: SkeletonFeedPost[]; cursor?: string }> => { + const { ref } = db.db.dynamic + + const keyset = new FeedKeyset(ref('feed_item.sortAt'), ref('feed_item.cid')) + const sortFrom = keyset.unpack(cursor)?.primary + + let followQb = db.db + .selectFrom('feed_item') + .innerJoin('follow', 'follow.subjectDid', 'feed_item.originatorDid') + .where('follow.creator', '=', viewer) + .innerJoin('post', 'post.uri', 'feed_item.postUri') + .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom, 1)) + .selectAll('feed_item') + .select([ + 'post.replyRoot', + 'post.replyParent', + 'post.creator as postAuthorDid', + ]) + + followQb = paginate(followQb, { + limit, + cursor, + keyset, + tryIndex: true, + }) + + let selfQb = db.db + .selectFrom('feed_item') + .innerJoin('post', 'post.uri', 'feed_item.postUri') + .where('feed_item.originatorDid', '=', viewer) + .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom, 1)) + .selectAll('feed_item') + .select([ + 'post.replyRoot', + 'post.replyParent', + 'post.creator as postAuthorDid', + ]) + + selfQb = paginate(selfQb, { + limit: Math.min(limit, 10), + cursor, + keyset, + tryIndex: true, + }) + + const [followRes, selfRes] = await Promise.all([ + followQb.execute(), + selfQb.execute(), + ]) + + const feedItems = [...followRes, ...selfRes] + .sort((a, b) => { + if (a.sortAt > b.sortAt) return -1 + if (a.sortAt < b.sortAt) return 1 + return a.cid > b.cid ? -1 : 1 + }) + .slice(0, limit) + const feed = feedItems.map((item) => ({ + post: item.postUri, + reason: + item.uri === item.postUri + ? undefined + : { + $type: 'app.bsky.feed.defs#skeletonReasonRepost', + repost: item.uri, + }, + })) + + return { + cursor: keyset.packFromResult(feedItems), + feed, + } +} diff --git a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts index c86e7433bc6..448af17e5f2 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts @@ -1,7 +1,6 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { FeedKeyset, getFeedDateThreshold } from '../util/feed' -import { paginate } from '../../../../db/pagination' +import { getTimelineSkeleton } from '../feed/getTimeline' // THIS IS A TEMPORARY UNSPECCED ROUTE export default function (server: Server, ctx: AppContext) { @@ -11,83 +10,10 @@ export default function (server: Server, ctx: AppContext) { const { limit, cursor } = params const viewer = auth.credentials.did - const db = ctx.db.db - const { ref } = db.dynamic - - const keyset = new FeedKeyset( - ref('feed_item.sortAt'), - ref('feed_item.cid'), - ) - const sortFrom = keyset.unpack(cursor)?.primary - - let followQb = db - .selectFrom('feed_item') - .innerJoin('follow', 'follow.subjectDid', 'feed_item.originatorDid') - .where('follow.creator', '=', viewer) - .innerJoin('post', 'post.uri', 'feed_item.postUri') - .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom, 1)) - .selectAll('feed_item') - .select([ - 'post.replyRoot', - 'post.replyParent', - 'post.creator as postAuthorDid', - ]) - - followQb = paginate(followQb, { - limit, - cursor, - keyset, - tryIndex: true, - }) - - let selfQb = ctx.db.db - .selectFrom('feed_item') - .innerJoin('post', 'post.uri', 'feed_item.postUri') - .where('feed_item.originatorDid', '=', viewer) - .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom, 1)) - .selectAll('feed_item') - .select([ - 'post.replyRoot', - 'post.replyParent', - 'post.creator as postAuthorDid', - ]) - - selfQb = paginate(selfQb, { - limit: Math.min(limit, 10), - cursor, - keyset, - tryIndex: true, - }) - - const [followRes, selfRes] = await Promise.all([ - followQb.execute(), - selfQb.execute(), - ]) - - const feedItems = [...followRes, ...selfRes] - .sort((a, b) => { - if (a.sortAt > b.sortAt) return -1 - if (a.sortAt < b.sortAt) return 1 - return a.cid > b.cid ? -1 : 1 - }) - .slice(0, limit) - - const feed = feedItems.map((item) => ({ - post: item.postUri, - reason: - item.uri === item.postUri - ? undefined - : { - $type: 'app.bsky.feed.defs#skeletonReasonRepost', - repost: item.uri, - }, - })) + const skeleton = await getTimelineSkeleton(ctx.db, viewer, limit, cursor) return { encoding: 'application/json', - body: { - cursor: keyset.packFromResult(feedItems), - feed, - }, + body: skeleton, } }, }) diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index 10d42731ea7..bdd15f75c53 100644 --- a/packages/bsky/src/services/feed/index.ts +++ b/packages/bsky/src/services/feed/index.ts @@ -23,7 +23,10 @@ import { isViewRecord, } from '../../lexicon/types/app/bsky/embed/record' import { isMain as isEmbedRecordWithMedia } from '../../lexicon/types/app/bsky/embed/recordWithMedia' -import { FeedViewPost } from '../../lexicon/types/app/bsky/feed/defs' +import { + FeedViewPost, + SkeletonFeedPost, +} from '../../lexicon/types/app/bsky/feed/defs' import { ActorInfoMap, PostInfoMap, @@ -330,6 +333,53 @@ export class FeedService { }, {} as PostViews) } + async filterAndGetFeedItems( + uris: string[], + requester: string, + ): Promise> { + if (uris.length < 1) return {} + const { ref } = this.db.db.dynamic + const feedItems = await this.selectFeedItemQb() + .where('feed_item.uri', 'in', uris) + .where((qb) => + // Hide posts and reposts of or by muted actors + this.services.graph.whereNotMuted(qb, requester, [ + ref('post.creator'), + ref('originatorDid'), + ]), + ) + .whereNotExists( + this.services.graph.blockQb(requester, [ + ref('post.creator'), + ref('originatorDid'), + ]), + ) + .execute() + return feedItems.reduce((acc, item) => { + return Object.assign(acc, { [item.uri]: item }) + }, {} as Record) + } + + // @TODO enforce limit elsewhere + async cleanFeedSkeleton( + skeleton: SkeletonFeedPost[], + limit: number, + requester: string, + ): Promise { + skeleton = skeleton.slice(0, limit) + const feedItemUris = skeleton.map(getSkeleFeedItemUri) + const feedItems = await this.filterAndGetFeedItems(feedItemUris, requester) + + const cleaned: FeedRow[] = [] + for (const skeleItem of skeleton) { + const feedItem = feedItems[getSkeleFeedItemUri(skeleItem)] + if (feedItem && feedItem.postUri === skeleItem.post) { + cleaned.push(feedItem) + } + } + return cleaned + } + async hydrateFeed( items: FeedRow[], viewer: string | null, @@ -641,3 +691,9 @@ function applyEmbedBlock( } return view } + +function getSkeleFeedItemUri(item: SkeletonFeedPost) { + return typeof item.reason?.repost === 'string' + ? item.reason.repost + : item.post +} From ba21ed6271e45db8a88ce8309b7a488c67d88228 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Aug 2023 15:48:17 -0500 Subject: [PATCH 096/237] add filter param to getAuthorFeed --- lexicons/app/bsky/feed/getAuthorFeed.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lexicons/app/bsky/feed/getAuthorFeed.json b/lexicons/app/bsky/feed/getAuthorFeed.json index 3713cb67c63..2c47343bb21 100644 --- a/lexicons/app/bsky/feed/getAuthorFeed.json +++ b/lexicons/app/bsky/feed/getAuthorFeed.json @@ -11,7 +11,8 @@ "properties": { "actor": {"type": "string", "format": "at-identifier"}, "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "cursor": {"type": "string"}, + "filter": {"type": "string", "enum": ["posts", "post_and_replies", "media"], "default": "posts"} } }, "output": { From c06a306d75d42f2a55feead19a2609d2833a6246 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Aug 2023 15:59:21 -0500 Subject: [PATCH 097/237] use current default --- lexicons/app/bsky/feed/getAuthorFeed.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lexicons/app/bsky/feed/getAuthorFeed.json b/lexicons/app/bsky/feed/getAuthorFeed.json index 2c47343bb21..5812d07d725 100644 --- a/lexicons/app/bsky/feed/getAuthorFeed.json +++ b/lexicons/app/bsky/feed/getAuthorFeed.json @@ -12,7 +12,7 @@ "actor": {"type": "string", "format": "at-identifier"}, "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, "cursor": {"type": "string"}, - "filter": {"type": "string", "enum": ["posts", "post_and_replies", "media"], "default": "posts"} + "filter": {"type": "string", "enum": ["post_and_replies", "posts", "media"], "default": "posts_and_replies"} } }, "output": { From 101bd9c7a1c7a0b7e64f8ab09cfbc9db476fb3b2 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Aug 2023 16:01:16 -0500 Subject: [PATCH 098/237] consistent naming --- lexicons/app/bsky/feed/getAuthorFeed.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lexicons/app/bsky/feed/getAuthorFeed.json b/lexicons/app/bsky/feed/getAuthorFeed.json index 5812d07d725..93da841d793 100644 --- a/lexicons/app/bsky/feed/getAuthorFeed.json +++ b/lexicons/app/bsky/feed/getAuthorFeed.json @@ -12,7 +12,7 @@ "actor": {"type": "string", "format": "at-identifier"}, "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, "cursor": {"type": "string"}, - "filter": {"type": "string", "enum": ["post_and_replies", "posts", "media"], "default": "posts_and_replies"} + "filter": {"type": "string", "enum": ["posts_and_replies", "posts", "media"], "default": "posts_and_replies"} } }, "output": { From 5edfb1a49dd35ec3f5718eb691870449eda877ee Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 8 Aug 2023 11:55:15 -0500 Subject: [PATCH 099/237] use knownValues --- lexicons/app/bsky/feed/getAuthorFeed.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lexicons/app/bsky/feed/getAuthorFeed.json b/lexicons/app/bsky/feed/getAuthorFeed.json index 93da841d793..828a1efc07d 100644 --- a/lexicons/app/bsky/feed/getAuthorFeed.json +++ b/lexicons/app/bsky/feed/getAuthorFeed.json @@ -12,7 +12,7 @@ "actor": {"type": "string", "format": "at-identifier"}, "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, "cursor": {"type": "string"}, - "filter": {"type": "string", "enum": ["posts_and_replies", "posts", "media"], "default": "posts_and_replies"} + "filter": {"type": "string", "knownValues": ["posts_and_replies", "posts", "media"], "default": "posts_and_replies"} } }, "output": { From 7f84fb215e570a300bc2f647f2077e7895275e44 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Aug 2023 16:03:00 -0500 Subject: [PATCH 100/237] codegen --- packages/api/src/client/lexicons.ts | 5 +++++ packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts | 1 + packages/bsky/src/lexicon/lexicons.ts | 5 +++++ .../bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts | 1 + packages/pds/src/lexicon/lexicons.ts | 5 +++++ .../pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts | 1 + 6 files changed, 18 insertions(+) diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 128330e214e..bfe0d170f5f 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -4752,6 +4752,11 @@ export const schemaDict = { cursor: { type: 'string', }, + filter: { + type: 'string', + enum: ['posts_and_replies', 'posts', 'media'], + default: 'posts_and_replies', + }, }, }, output: { diff --git a/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts b/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts index 442a7efcc08..49ef8f84764 100644 --- a/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts @@ -12,6 +12,7 @@ export interface QueryParams { actor: string limit?: number cursor?: string + filter?: 'posts_and_replies' | 'posts' | 'media' } export type InputSchema = undefined diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 128330e214e..bfe0d170f5f 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -4752,6 +4752,11 @@ export const schemaDict = { cursor: { type: 'string', }, + filter: { + type: 'string', + enum: ['posts_and_replies', 'posts', 'media'], + default: 'posts_and_replies', + }, }, }, output: { diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts index ce56c2667fd..dc49c1799e4 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts @@ -13,6 +13,7 @@ export interface QueryParams { actor: string limit: number cursor?: string + filter: 'posts_and_replies' | 'posts' | 'media' } export type InputSchema = undefined diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 128330e214e..bfe0d170f5f 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -4752,6 +4752,11 @@ export const schemaDict = { cursor: { type: 'string', }, + filter: { + type: 'string', + enum: ['posts_and_replies', 'posts', 'media'], + default: 'posts_and_replies', + }, }, }, output: { diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts index ce56c2667fd..dc49c1799e4 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts @@ -13,6 +13,7 @@ export interface QueryParams { actor: string limit: number cursor?: string + filter: 'posts_and_replies' | 'posts' | 'media' } export type InputSchema = undefined From ff11f5fae53d4dfce84a7ef2c5ca0676df5ab6e1 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Aug 2023 17:21:38 -0500 Subject: [PATCH 101/237] add queries and test to app view --- .../src/api/app/bsky/feed/getAuthorFeed.ts | 15 ++++++- packages/bsky/tests/views/author-feed.test.ts | 40 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 5fbabd9f1e5..10b90b5f015 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getAuthorFeed({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth }) => { - const { actor, limit, cursor } = params + const { actor, limit, cursor, filter } = params const viewer = auth.credentials.did const db = ctx.db.db const { ref } = db.dynamic @@ -46,10 +46,23 @@ export default function (server: Server, ctx: AppContext) { } } + // defaults to posts, reposts, and replies let feedItemsQb = feedService .selectFeedItemQb() .where('originatorDid', '=', did) + if (filter === 'media') { + // only posts with media + feedItemsQb = feedItemsQb.innerJoin( + 'post_embed_image', + 'post_embed_image.postUri', + 'feed_item.postUri', + ) + } else if (filter === 'posts') { + // only posts, no replies + feedItemsQb = feedItemsQb.where('post.replyParent', 'is', null) + } + if (viewer !== null) { feedItemsQb = feedItemsQb.where((qb) => // Hide reposts of muted content diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index 11c79189c4d..614baa1fcd7 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -4,6 +4,8 @@ import { forSnapshot, paginateAll, stripViewerFromPost } from '../_util' import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' +import { isRecord } from '../../src/lexicon/types/app/bsky/feed/post' +import { validateMain as isEmbedRecordWithMedia } from '../../src/lexicon/types/app/bsky/embed/recordWithMedia' describe('pds author feed views', () => { let network: TestNetwork @@ -232,4 +234,42 @@ describe('pds author feed views', () => { }, ) }) + + it('can filter by type', async () => { + const { data: allFeed } = await agent.api.app.bsky.feed.getAuthorFeed({ + actor: carol, + }) + + expect( + allFeed.feed.some(({ post }) => { + return isRecord(post.record) && Boolean(post.record.reply) + }), + ).toBeTruthy() + expect( + allFeed.feed.some(({ post }) => { + return isEmbedRecordWithMedia(post.record) + }), + ).toBeTruthy() + + const { data: mediaFeed } = await agent.api.app.bsky.feed.getAuthorFeed({ + actor: carol, + filter: 'media', + }) + + expect( + mediaFeed.feed.every(({ post }) => { + return isEmbedRecordWithMedia(post.record) + }), + ).toBeTruthy() + + const { data: postsOnlyFeed } = await agent.api.app.bsky.feed.getAuthorFeed( + { actor: carol, filter: 'posts' }, + ) + + expect( + postsOnlyFeed.feed.every(({ post }) => { + return isRecord(post.record) && !Boolean(post.record.reply) + }), + ).toBeTruthy() + }) }) From 59c435648d4aad339571abc16de13dc3d69af2d6 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 8 Aug 2023 12:01:14 -0500 Subject: [PATCH 102/237] codegen again --- packages/api/src/client/lexicons.ts | 2 +- packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts | 2 +- packages/bsky/src/lexicon/lexicons.ts | 2 +- packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts | 2 +- packages/pds/src/lexicon/lexicons.ts | 2 +- packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index bfe0d170f5f..9f3a0262c72 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -4754,7 +4754,7 @@ export const schemaDict = { }, filter: { type: 'string', - enum: ['posts_and_replies', 'posts', 'media'], + knownValues: ['posts_and_replies', 'posts', 'media'], default: 'posts_and_replies', }, }, diff --git a/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts b/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts index 49ef8f84764..571ef51b8b7 100644 --- a/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts @@ -12,7 +12,7 @@ export interface QueryParams { actor: string limit?: number cursor?: string - filter?: 'posts_and_replies' | 'posts' | 'media' + filter?: 'posts_and_replies' | 'posts' | 'media' | (string & {}) } export type InputSchema = undefined diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index bfe0d170f5f..9f3a0262c72 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -4754,7 +4754,7 @@ export const schemaDict = { }, filter: { type: 'string', - enum: ['posts_and_replies', 'posts', 'media'], + knownValues: ['posts_and_replies', 'posts', 'media'], default: 'posts_and_replies', }, }, diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts index dc49c1799e4..72419e83a9d 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts @@ -13,7 +13,7 @@ export interface QueryParams { actor: string limit: number cursor?: string - filter: 'posts_and_replies' | 'posts' | 'media' + filter: 'posts_and_replies' | 'posts' | 'media' | (string & {}) } export type InputSchema = undefined diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index bfe0d170f5f..9f3a0262c72 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -4754,7 +4754,7 @@ export const schemaDict = { }, filter: { type: 'string', - enum: ['posts_and_replies', 'posts', 'media'], + knownValues: ['posts_and_replies', 'posts', 'media'], default: 'posts_and_replies', }, }, diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts index dc49c1799e4..72419e83a9d 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts @@ -13,7 +13,7 @@ export interface QueryParams { actor: string limit: number cursor?: string - filter: 'posts_and_replies' | 'posts' | 'media' + filter: 'posts_and_replies' | 'posts' | 'media' | (string & {}) } export type InputSchema = undefined From 9e2b14765a76b72329c2b475f86b310e1f9750a8 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 8 Aug 2023 12:14:07 -0500 Subject: [PATCH 103/237] add query and test to pds --- .../api/app/bsky/feed/getAuthorFeed.ts | 13 +++++ packages/pds/tests/views/author-feed.test.ts | 47 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index 6d7353c139c..a73efa3cd67 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -32,8 +32,21 @@ export default function (server: Server, ctx: AppContext) { const feedService = ctx.services.appView.feed(ctx.db) const graphService = ctx.services.appView.graph(ctx.db) + // defaults to posts, reposts, and replies let feedItemsQb = getFeedItemsQb(ctx, { actor }) + if (params.filter === 'media') { + // only posts with media + feedItemsQb = feedItemsQb.innerJoin( + 'post_embed_image', + 'post_embed_image.postUri', + 'feed_item.postUri', + ) + } else if (params.filter === 'posts') { + // only posts, no replies + feedItemsQb = feedItemsQb.where('post.replyParent', 'is', null) + } + // for access-based auth, enforce blocks and mutes if (requester) { await assertNoBlocks(ctx, { requester, actor }) diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index 4af028dc03f..219b6569a29 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -9,6 +9,8 @@ import { } from '../_util' import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' +import { isRecord } from '../../src/lexicon/types/app/bsky/feed/post' +import { validateMain as isEmbedRecordWithMedia } from '../../src/lexicon/types/app/bsky/embed/recordWithMedia' describe('pds author feed views', () => { let agent: AtpAgent @@ -275,4 +277,49 @@ describe('pds author feed views', () => { // Cleanup await reverseModerationAction(takedownAction.id) }) + + it('can filter by type', async () => { + const { data: allFeed } = await agent.api.app.bsky.feed.getAuthorFeed( + { actor: carol }, + { headers: sc.getHeaders(alice) }, + ) + + expect( + allFeed.feed.some(({ post }) => { + return isRecord(post.record) && Boolean(post.record.reply) + }), + ).toBeTruthy() + expect( + allFeed.feed.some(({ post }) => { + return isEmbedRecordWithMedia(post.record) + }), + ).toBeTruthy() + + const { data: mediaFeed } = await agent.api.app.bsky.feed.getAuthorFeed( + { + actor: carol, + filter: 'media', + }, + { + headers: sc.getHeaders(alice), + }, + ) + + expect( + mediaFeed.feed.every(({ post }) => { + return isEmbedRecordWithMedia(post.record) + }), + ).toBeTruthy() + + const { data: postsOnlyFeed } = await agent.api.app.bsky.feed.getAuthorFeed( + { actor: carol, filter: 'posts' }, + { headers: sc.getHeaders(alice) }, + ) + + expect( + postsOnlyFeed.feed.every(({ post }) => { + return isRecord(post.record) && !Boolean(post.record.reply) + }), + ).toBeTruthy() + }) }) From 5083705a378285db4392be9d88d847d33aece535 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 8 Aug 2023 12:20:08 -0500 Subject: [PATCH 104/237] use more specific naming --- lexicons/app/bsky/feed/getAuthorFeed.json | 2 +- packages/api/src/client/lexicons.ts | 6 +++++- .../api/src/client/types/app/bsky/feed/getAuthorFeed.ts | 6 +++++- packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts | 4 ++-- packages/bsky/src/lexicon/lexicons.ts | 6 +++++- .../bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts | 6 +++++- packages/bsky/tests/views/author-feed.test.ts | 4 ++-- .../pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts | 4 ++-- packages/pds/src/lexicon/lexicons.ts | 6 +++++- .../pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts | 6 +++++- packages/pds/tests/views/author-feed.test.ts | 4 ++-- 11 files changed, 39 insertions(+), 15 deletions(-) diff --git a/lexicons/app/bsky/feed/getAuthorFeed.json b/lexicons/app/bsky/feed/getAuthorFeed.json index 828a1efc07d..4e004fa50de 100644 --- a/lexicons/app/bsky/feed/getAuthorFeed.json +++ b/lexicons/app/bsky/feed/getAuthorFeed.json @@ -12,7 +12,7 @@ "actor": {"type": "string", "format": "at-identifier"}, "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, "cursor": {"type": "string"}, - "filter": {"type": "string", "knownValues": ["posts_and_replies", "posts", "media"], "default": "posts_and_replies"} + "filter": {"type": "string", "knownValues": ["posts_and_replies", "posts_only", "posts_with_media"], "default": "posts_and_replies"} } }, "output": { diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 9f3a0262c72..6129cfe037e 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -4754,7 +4754,11 @@ export const schemaDict = { }, filter: { type: 'string', - knownValues: ['posts_and_replies', 'posts', 'media'], + knownValues: [ + 'posts_and_replies', + 'posts_only', + 'posts_with_media', + ], default: 'posts_and_replies', }, }, diff --git a/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts b/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts index 571ef51b8b7..f6de54939be 100644 --- a/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts @@ -12,7 +12,11 @@ export interface QueryParams { actor: string limit?: number cursor?: string - filter?: 'posts_and_replies' | 'posts' | 'media' | (string & {}) + filter?: + | 'posts_and_replies' + | 'posts_only' + | 'posts_with_media' + | (string & {}) } export type InputSchema = undefined diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 10b90b5f015..0aeb5a8e2f6 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -51,14 +51,14 @@ export default function (server: Server, ctx: AppContext) { .selectFeedItemQb() .where('originatorDid', '=', did) - if (filter === 'media') { + if (filter === 'posts_with_media') { // only posts with media feedItemsQb = feedItemsQb.innerJoin( 'post_embed_image', 'post_embed_image.postUri', 'feed_item.postUri', ) - } else if (filter === 'posts') { + } else if (filter === 'posts_only') { // only posts, no replies feedItemsQb = feedItemsQb.where('post.replyParent', 'is', null) } diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 9f3a0262c72..6129cfe037e 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -4754,7 +4754,11 @@ export const schemaDict = { }, filter: { type: 'string', - knownValues: ['posts_and_replies', 'posts', 'media'], + knownValues: [ + 'posts_and_replies', + 'posts_only', + 'posts_with_media', + ], default: 'posts_and_replies', }, }, diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts index 72419e83a9d..2160e3bcdc3 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts @@ -13,7 +13,11 @@ export interface QueryParams { actor: string limit: number cursor?: string - filter: 'posts_and_replies' | 'posts' | 'media' | (string & {}) + filter: + | 'posts_and_replies' + | 'posts_only' + | 'posts_with_media' + | (string & {}) } export type InputSchema = undefined diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index 614baa1fcd7..bf878498ae9 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -253,7 +253,7 @@ describe('pds author feed views', () => { const { data: mediaFeed } = await agent.api.app.bsky.feed.getAuthorFeed({ actor: carol, - filter: 'media', + filter: 'posts_with_media', }) expect( @@ -263,7 +263,7 @@ describe('pds author feed views', () => { ).toBeTruthy() const { data: postsOnlyFeed } = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: carol, filter: 'posts' }, + { actor: carol, filter: 'posts_only' }, ) expect( diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index a73efa3cd67..5eaf0d0a76a 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -35,14 +35,14 @@ export default function (server: Server, ctx: AppContext) { // defaults to posts, reposts, and replies let feedItemsQb = getFeedItemsQb(ctx, { actor }) - if (params.filter === 'media') { + if (params.filter === 'posts_with_media') { // only posts with media feedItemsQb = feedItemsQb.innerJoin( 'post_embed_image', 'post_embed_image.postUri', 'feed_item.postUri', ) - } else if (params.filter === 'posts') { + } else if (params.filter === 'posts_only') { // only posts, no replies feedItemsQb = feedItemsQb.where('post.replyParent', 'is', null) } diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 9f3a0262c72..6129cfe037e 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -4754,7 +4754,11 @@ export const schemaDict = { }, filter: { type: 'string', - knownValues: ['posts_and_replies', 'posts', 'media'], + knownValues: [ + 'posts_and_replies', + 'posts_only', + 'posts_with_media', + ], default: 'posts_and_replies', }, }, diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts index 72419e83a9d..2160e3bcdc3 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts @@ -13,7 +13,11 @@ export interface QueryParams { actor: string limit: number cursor?: string - filter: 'posts_and_replies' | 'posts' | 'media' | (string & {}) + filter: + | 'posts_and_replies' + | 'posts_only' + | 'posts_with_media' + | (string & {}) } export type InputSchema = undefined diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index 219b6569a29..5e81bca6882 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -298,7 +298,7 @@ describe('pds author feed views', () => { const { data: mediaFeed } = await agent.api.app.bsky.feed.getAuthorFeed( { actor: carol, - filter: 'media', + filter: 'posts_with_media', }, { headers: sc.getHeaders(alice), @@ -312,7 +312,7 @@ describe('pds author feed views', () => { ).toBeTruthy() const { data: postsOnlyFeed } = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: carol, filter: 'posts' }, + { actor: carol, filter: 'posts_only' }, { headers: sc.getHeaders(alice) }, ) From 975b0f40edfc43e2718dfe733e3a8fd10502b267 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 8 Aug 2023 12:22:22 -0500 Subject: [PATCH 105/237] fix lint --- packages/bsky/tests/views/author-feed.test.ts | 2 +- packages/pds/tests/views/author-feed.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index bf878498ae9..50de4f648e8 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -268,7 +268,7 @@ describe('pds author feed views', () => { expect( postsOnlyFeed.feed.every(({ post }) => { - return isRecord(post.record) && !Boolean(post.record.reply) + return isRecord(post.record) && !post.record.reply }), ).toBeTruthy() }) diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index 5e81bca6882..9481c451a9a 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -318,7 +318,7 @@ describe('pds author feed views', () => { expect( postsOnlyFeed.feed.every(({ post }) => { - return isRecord(post.record) && !Boolean(post.record.reply) + return isRecord(post.record) && !post.record.reply }), ).toBeTruthy() }) From 25458ade66154b3a93a2d41552ef898725642680 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 8 Aug 2023 15:30:42 -0500 Subject: [PATCH 106/237] Fix open handles in appview tests (#1452) fix open handles in appview tests --- packages/dev-env/src/bsky.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index 46a2dd0f793..67c5fb5067b 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -170,7 +170,7 @@ export class TestBsky { async close() { await this.server.destroy({ skipDb: true }) await this.ingester.destroy({ skipDb: true }) - this.indexer.destroy() // closes shared db + await this.indexer.destroy() // closes shared db } } From 92f5979d4493f851c8ca38b8063eb585c197aa76 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 8 Aug 2023 15:41:48 -0500 Subject: [PATCH 107/237] update naming based on feedback --- lexicons/app/bsky/feed/getAuthorFeed.json | 2 +- packages/api/src/client/lexicons.ts | 8 ++------ .../api/src/client/types/app/bsky/feed/getAuthorFeed.ts | 6 +----- packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts | 2 +- packages/bsky/src/lexicon/lexicons.ts | 8 ++------ .../bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts | 6 +----- packages/bsky/tests/views/author-feed.test.ts | 2 +- .../pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts | 2 +- packages/pds/src/lexicon/lexicons.ts | 8 ++------ .../pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts | 6 +----- packages/pds/tests/views/author-feed.test.ts | 2 +- 11 files changed, 14 insertions(+), 38 deletions(-) diff --git a/lexicons/app/bsky/feed/getAuthorFeed.json b/lexicons/app/bsky/feed/getAuthorFeed.json index 4e004fa50de..18d1a3bf552 100644 --- a/lexicons/app/bsky/feed/getAuthorFeed.json +++ b/lexicons/app/bsky/feed/getAuthorFeed.json @@ -12,7 +12,7 @@ "actor": {"type": "string", "format": "at-identifier"}, "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, "cursor": {"type": "string"}, - "filter": {"type": "string", "knownValues": ["posts_and_replies", "posts_only", "posts_with_media"], "default": "posts_and_replies"} + "filter": {"type": "string", "knownValues": ["posts", "posts_no_replies", "posts_with_media"], "default": "posts"} } }, "output": { diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 6129cfe037e..a44d980eb02 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -4754,12 +4754,8 @@ export const schemaDict = { }, filter: { type: 'string', - knownValues: [ - 'posts_and_replies', - 'posts_only', - 'posts_with_media', - ], - default: 'posts_and_replies', + knownValues: ['posts', 'posts_no_replies', 'posts_with_media'], + default: 'posts', }, }, }, diff --git a/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts b/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts index f6de54939be..5e74971609a 100644 --- a/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts @@ -12,11 +12,7 @@ export interface QueryParams { actor: string limit?: number cursor?: string - filter?: - | 'posts_and_replies' - | 'posts_only' - | 'posts_with_media' - | (string & {}) + filter?: 'posts' | 'posts_no_replies' | 'posts_with_media' | (string & {}) } export type InputSchema = undefined diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 0aeb5a8e2f6..1172778b7d2 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -58,7 +58,7 @@ export default function (server: Server, ctx: AppContext) { 'post_embed_image.postUri', 'feed_item.postUri', ) - } else if (filter === 'posts_only') { + } else if (filter === 'posts_no_replies') { // only posts, no replies feedItemsQb = feedItemsQb.where('post.replyParent', 'is', null) } diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 6129cfe037e..a44d980eb02 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -4754,12 +4754,8 @@ export const schemaDict = { }, filter: { type: 'string', - knownValues: [ - 'posts_and_replies', - 'posts_only', - 'posts_with_media', - ], - default: 'posts_and_replies', + knownValues: ['posts', 'posts_no_replies', 'posts_with_media'], + default: 'posts', }, }, }, diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts index 2160e3bcdc3..c4846386eb9 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts @@ -13,11 +13,7 @@ export interface QueryParams { actor: string limit: number cursor?: string - filter: - | 'posts_and_replies' - | 'posts_only' - | 'posts_with_media' - | (string & {}) + filter: 'posts' | 'posts_no_replies' | 'posts_with_media' | (string & {}) } export type InputSchema = undefined diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index 50de4f648e8..bf7dfa159a3 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -263,7 +263,7 @@ describe('pds author feed views', () => { ).toBeTruthy() const { data: postsOnlyFeed } = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: carol, filter: 'posts_only' }, + { actor: carol, filter: 'posts_no_replies' }, ) expect( diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index 5eaf0d0a76a..eee565e9acf 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -42,7 +42,7 @@ export default function (server: Server, ctx: AppContext) { 'post_embed_image.postUri', 'feed_item.postUri', ) - } else if (params.filter === 'posts_only') { + } else if (params.filter === 'posts_no_replies') { // only posts, no replies feedItemsQb = feedItemsQb.where('post.replyParent', 'is', null) } diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 6129cfe037e..a44d980eb02 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -4754,12 +4754,8 @@ export const schemaDict = { }, filter: { type: 'string', - knownValues: [ - 'posts_and_replies', - 'posts_only', - 'posts_with_media', - ], - default: 'posts_and_replies', + knownValues: ['posts', 'posts_no_replies', 'posts_with_media'], + default: 'posts', }, }, }, diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts index 2160e3bcdc3..c4846386eb9 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts @@ -13,11 +13,7 @@ export interface QueryParams { actor: string limit: number cursor?: string - filter: - | 'posts_and_replies' - | 'posts_only' - | 'posts_with_media' - | (string & {}) + filter: 'posts' | 'posts_no_replies' | 'posts_with_media' | (string & {}) } export type InputSchema = undefined diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index 9481c451a9a..ddd515339e7 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -312,7 +312,7 @@ describe('pds author feed views', () => { ).toBeTruthy() const { data: postsOnlyFeed } = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: carol, filter: 'posts_only' }, + { actor: carol, filter: 'posts_no_replies' }, { headers: sc.getHeaders(alice) }, ) From 399dfdbcc51812ffc2471147037257c3326c43dc Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 8 Aug 2023 16:03:44 -0500 Subject: [PATCH 108/237] fix tests --- packages/bsky/tests/views/author-feed.test.ts | 9 ++++++--- packages/pds/tests/views/author-feed.test.ts | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index bf7dfa159a3..be1e92ff404 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -5,7 +5,8 @@ import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { isRecord } from '../../src/lexicon/types/app/bsky/feed/post' -import { validateMain as isEmbedRecordWithMedia } from '../../src/lexicon/types/app/bsky/embed/recordWithMedia' +import { isView as isEmbedRecordWithMedia } from '../../src/lexicon/types/app/bsky/embed/recordWithMedia' +import { isView as isImageEmbed } from '../../src/lexicon/types/app/bsky/embed/images' describe('pds author feed views', () => { let network: TestNetwork @@ -247,7 +248,8 @@ describe('pds author feed views', () => { ).toBeTruthy() expect( allFeed.feed.some(({ post }) => { - return isEmbedRecordWithMedia(post.record) + return isEmbedRecordWithMedia(post.embed) && + isImageEmbed(post.embed?.media) }), ).toBeTruthy() @@ -258,7 +260,8 @@ describe('pds author feed views', () => { expect( mediaFeed.feed.every(({ post }) => { - return isEmbedRecordWithMedia(post.record) + return isEmbedRecordWithMedia(post.embed) && + isImageEmbed(post.embed?.media) }), ).toBeTruthy() diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index ddd515339e7..0bd5ba85baf 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -10,7 +10,8 @@ import { import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' import { isRecord } from '../../src/lexicon/types/app/bsky/feed/post' -import { validateMain as isEmbedRecordWithMedia } from '../../src/lexicon/types/app/bsky/embed/recordWithMedia' +import { isView as isEmbedRecordWithMedia } from '../../src/lexicon/types/app/bsky/embed/recordWithMedia' +import { isView as isImageEmbed } from '../../src/lexicon/types/app/bsky/embed/images' describe('pds author feed views', () => { let agent: AtpAgent @@ -291,7 +292,8 @@ describe('pds author feed views', () => { ).toBeTruthy() expect( allFeed.feed.some(({ post }) => { - return isEmbedRecordWithMedia(post.record) + return isEmbedRecordWithMedia(post.embed) && + isImageEmbed(post.embed?.media) }), ).toBeTruthy() @@ -307,7 +309,8 @@ describe('pds author feed views', () => { expect( mediaFeed.feed.every(({ post }) => { - return isEmbedRecordWithMedia(post.record) + return isEmbedRecordWithMedia(post.embed) && + isImageEmbed(post.embed?.media) }), ).toBeTruthy() From 08ea9790d98cfd025b1490aa55cfa40dba4c9b7d Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 8 Aug 2023 16:13:47 -0500 Subject: [PATCH 109/237] use where exists subquery --- packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts | 7 +++---- .../pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 1172778b7d2..b1e1f447630 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -53,10 +53,9 @@ export default function (server: Server, ctx: AppContext) { if (filter === 'posts_with_media') { // only posts with media - feedItemsQb = feedItemsQb.innerJoin( - 'post_embed_image', - 'post_embed_image.postUri', - 'feed_item.postUri', + feedItemsQb = feedItemsQb.whereExists(qb => + qb.selectFrom('post_embed_image') + .where('post_embed_image.postUri', '=', 'feed_item.postUri') ) } else if (filter === 'posts_no_replies') { // only posts, no replies diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index eee565e9acf..f7588817421 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -37,10 +37,9 @@ export default function (server: Server, ctx: AppContext) { if (params.filter === 'posts_with_media') { // only posts with media - feedItemsQb = feedItemsQb.innerJoin( - 'post_embed_image', - 'post_embed_image.postUri', - 'feed_item.postUri', + feedItemsQb = feedItemsQb.whereExists(qb => + qb.selectFrom('post_embed_image') + .where('post_embed_image.postUri', '=', 'feed_item.postUri') ) } else if (params.filter === 'posts_no_replies') { // only posts, no replies From 5f2fc6c1ae8aef1fed62d811bc74b391de3f94a4 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 8 Aug 2023 16:14:27 -0500 Subject: [PATCH 110/237] format --- packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts | 7 ++++--- packages/bsky/tests/views/author-feed.test.ts | 10 ++++++---- .../src/app-view/api/app/bsky/feed/getAuthorFeed.ts | 7 ++++--- packages/pds/tests/views/author-feed.test.ts | 10 ++++++---- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index b1e1f447630..cca512bf5c7 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -53,9 +53,10 @@ export default function (server: Server, ctx: AppContext) { if (filter === 'posts_with_media') { // only posts with media - feedItemsQb = feedItemsQb.whereExists(qb => - qb.selectFrom('post_embed_image') - .where('post_embed_image.postUri', '=', 'feed_item.postUri') + feedItemsQb = feedItemsQb.whereExists((qb) => + qb + .selectFrom('post_embed_image') + .where('post_embed_image.postUri', '=', 'feed_item.postUri'), ) } else if (filter === 'posts_no_replies') { // only posts, no replies diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index be1e92ff404..b8041573cde 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -248,8 +248,9 @@ describe('pds author feed views', () => { ).toBeTruthy() expect( allFeed.feed.some(({ post }) => { - return isEmbedRecordWithMedia(post.embed) && - isImageEmbed(post.embed?.media) + return ( + isEmbedRecordWithMedia(post.embed) && isImageEmbed(post.embed?.media) + ) }), ).toBeTruthy() @@ -260,8 +261,9 @@ describe('pds author feed views', () => { expect( mediaFeed.feed.every(({ post }) => { - return isEmbedRecordWithMedia(post.embed) && - isImageEmbed(post.embed?.media) + return ( + isEmbedRecordWithMedia(post.embed) && isImageEmbed(post.embed?.media) + ) }), ).toBeTruthy() diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index f7588817421..3d0f0ad2e2c 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -37,9 +37,10 @@ export default function (server: Server, ctx: AppContext) { if (params.filter === 'posts_with_media') { // only posts with media - feedItemsQb = feedItemsQb.whereExists(qb => - qb.selectFrom('post_embed_image') - .where('post_embed_image.postUri', '=', 'feed_item.postUri') + feedItemsQb = feedItemsQb.whereExists((qb) => + qb + .selectFrom('post_embed_image') + .where('post_embed_image.postUri', '=', 'feed_item.postUri'), ) } else if (params.filter === 'posts_no_replies') { // only posts, no replies diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index 0bd5ba85baf..307570ff25b 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -292,8 +292,9 @@ describe('pds author feed views', () => { ).toBeTruthy() expect( allFeed.feed.some(({ post }) => { - return isEmbedRecordWithMedia(post.embed) && - isImageEmbed(post.embed?.media) + return ( + isEmbedRecordWithMedia(post.embed) && isImageEmbed(post.embed?.media) + ) }), ).toBeTruthy() @@ -309,8 +310,9 @@ describe('pds author feed views', () => { expect( mediaFeed.feed.every(({ post }) => { - return isEmbedRecordWithMedia(post.embed) && - isImageEmbed(post.embed?.media) + return ( + isEmbedRecordWithMedia(post.embed) && isImageEmbed(post.embed?.media) + ) }), ).toBeTruthy() From b70238476f40e3504dfc7c6e940665fa68c92609 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 8 Aug 2023 16:38:39 -0500 Subject: [PATCH 111/237] fix select cause --- packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts | 3 ++- packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index cca512bf5c7..42d1af5cd36 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -56,7 +56,8 @@ export default function (server: Server, ctx: AppContext) { feedItemsQb = feedItemsQb.whereExists((qb) => qb .selectFrom('post_embed_image') - .where('post_embed_image.postUri', '=', 'feed_item.postUri'), + .select('post_embed_image.postUri') + .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'), ) } else if (filter === 'posts_no_replies') { // only posts, no replies diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index 3d0f0ad2e2c..016d5f55487 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -40,7 +40,8 @@ export default function (server: Server, ctx: AppContext) { feedItemsQb = feedItemsQb.whereExists((qb) => qb .selectFrom('post_embed_image') - .where('post_embed_image.postUri', '=', 'feed_item.postUri'), + .select('post_embed_image.postUri') + .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'), ) } else if (params.filter === 'posts_no_replies') { // only posts, no replies From 55f33234e2398fbea20965acab9f9a44d49a3086 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 8 Aug 2023 16:40:45 -0500 Subject: [PATCH 112/237] fix naming --- lexicons/app/bsky/feed/getAuthorFeed.json | 2 +- packages/api/src/client/lexicons.ts | 8 ++++++-- .../api/src/client/types/app/bsky/feed/getAuthorFeed.ts | 6 +++++- packages/bsky/src/lexicon/lexicons.ts | 8 ++++++-- .../bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts | 6 +++++- packages/pds/src/lexicon/lexicons.ts | 8 ++++++-- .../pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts | 6 +++++- 7 files changed, 34 insertions(+), 10 deletions(-) diff --git a/lexicons/app/bsky/feed/getAuthorFeed.json b/lexicons/app/bsky/feed/getAuthorFeed.json index 18d1a3bf552..fe7d4cae59e 100644 --- a/lexicons/app/bsky/feed/getAuthorFeed.json +++ b/lexicons/app/bsky/feed/getAuthorFeed.json @@ -12,7 +12,7 @@ "actor": {"type": "string", "format": "at-identifier"}, "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, "cursor": {"type": "string"}, - "filter": {"type": "string", "knownValues": ["posts", "posts_no_replies", "posts_with_media"], "default": "posts"} + "filter": {"type": "string", "knownValues": ["posts_with_replies", "posts_no_replies", "posts_with_media"], "default": "posts_with_replies"} } }, "output": { diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index a44d980eb02..ed5d0a97f81 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -4754,8 +4754,12 @@ export const schemaDict = { }, filter: { type: 'string', - knownValues: ['posts', 'posts_no_replies', 'posts_with_media'], - default: 'posts', + knownValues: [ + 'posts_with_replies', + 'posts_no_replies', + 'posts_with_media', + ], + default: 'posts_with_replies', }, }, }, diff --git a/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts b/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts index 5e74971609a..a2107220efa 100644 --- a/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts @@ -12,7 +12,11 @@ export interface QueryParams { actor: string limit?: number cursor?: string - filter?: 'posts' | 'posts_no_replies' | 'posts_with_media' | (string & {}) + filter?: + | 'posts_with_replies' + | 'posts_no_replies' + | 'posts_with_media' + | (string & {}) } export type InputSchema = undefined diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index a44d980eb02..ed5d0a97f81 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -4754,8 +4754,12 @@ export const schemaDict = { }, filter: { type: 'string', - knownValues: ['posts', 'posts_no_replies', 'posts_with_media'], - default: 'posts', + knownValues: [ + 'posts_with_replies', + 'posts_no_replies', + 'posts_with_media', + ], + default: 'posts_with_replies', }, }, }, diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts index c4846386eb9..8c44be045ac 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts @@ -13,7 +13,11 @@ export interface QueryParams { actor: string limit: number cursor?: string - filter: 'posts' | 'posts_no_replies' | 'posts_with_media' | (string & {}) + filter: + | 'posts_with_replies' + | 'posts_no_replies' + | 'posts_with_media' + | (string & {}) } export type InputSchema = undefined diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index a44d980eb02..ed5d0a97f81 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -4754,8 +4754,12 @@ export const schemaDict = { }, filter: { type: 'string', - knownValues: ['posts', 'posts_no_replies', 'posts_with_media'], - default: 'posts', + knownValues: [ + 'posts_with_replies', + 'posts_no_replies', + 'posts_with_media', + ], + default: 'posts_with_replies', }, }, }, diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts index c4846386eb9..8c44be045ac 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts @@ -13,7 +13,11 @@ export interface QueryParams { actor: string limit: number cursor?: string - filter: 'posts' | 'posts_no_replies' | 'posts_with_media' | (string & {}) + filter: + | 'posts_with_replies' + | 'posts_no_replies' + | 'posts_with_media' + | (string & {}) } export type InputSchema = undefined From 4879ccb3652f032272236e2cad970eebc86aee8b Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 8 Aug 2023 17:56:21 -0500 Subject: [PATCH 113/237] Appview label cache (#1451) * add label cache to appview * add to dev env * fix test --- packages/bsky/src/context.ts | 6 ++ packages/bsky/src/index.ts | 11 ++- packages/bsky/src/indexer/services.ts | 2 +- packages/bsky/src/label-cache.ts | 90 +++++++++++++++++++ packages/bsky/src/services/actor/index.ts | 13 ++- packages/bsky/src/services/actor/views.ts | 9 +- packages/bsky/src/services/feed/index.ts | 15 ++-- packages/bsky/src/services/index.ts | 10 ++- packages/bsky/src/services/label/index.ts | 51 +++++++---- packages/dev-env/src/bsky.ts | 4 + .../pds/src/app-view/services/label/index.ts | 39 ++++---- packages/pds/tests/proxied/admin.test.ts | 8 +- 12 files changed, 204 insertions(+), 54 deletions(-) create mode 100644 packages/bsky/src/label-cache.ts diff --git a/packages/bsky/src/context.ts b/packages/bsky/src/context.ts index f5933cdcf1c..ce02fd41ffc 100644 --- a/packages/bsky/src/context.ts +++ b/packages/bsky/src/context.ts @@ -8,6 +8,7 @@ import * as auth from './auth' import DidSqlCache from './did-cache' import { BackgroundQueue } from './background' import { MountedAlgos } from './feed-gen/types' +import { LabelCache } from './label-cache' export class AppContext { constructor( @@ -18,6 +19,7 @@ export class AppContext { services: Services idResolver: IdResolver didCache: DidSqlCache + labelCache: LabelCache backgroundQueue: BackgroundQueue algos: MountedAlgos }, @@ -51,6 +53,10 @@ export class AppContext { return this.opts.didCache } + get labelCache(): LabelCache { + return this.opts.labelCache + } + get authVerifier() { return auth.authVerifier(this.idResolver, { aud: this.cfg.serverDid }) } diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 86ac01b8fd8..2bb115729b2 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -23,6 +23,7 @@ import { } from './image/invalidator' import { BackgroundQueue } from './background' import { MountedAlgos } from './feed-gen/types' +import { LabelCache } from './label-cache' export type { ServerConfigValues } from './config' export type { MountedAlgos } from './feed-gen/types' @@ -93,8 +94,13 @@ export class BskyAppView { } const backgroundQueue = new BackgroundQueue(db) + const labelCache = new LabelCache(db) - const services = createServices({ imgUriBuilder, imgInvalidator }) + const services = createServices({ + imgUriBuilder, + imgInvalidator, + labelCache, + }) const ctx = new AppContext({ db, @@ -103,6 +109,7 @@ export class BskyAppView { imgUriBuilder, idResolver, didCache, + labelCache, backgroundQueue, algos, }) @@ -149,6 +156,7 @@ export class BskyAppView { 'background queue stats', ) }, 10000) + this.ctx.labelCache.start() const server = this.app.listen(this.ctx.cfg.port) this.server = server server.keepAliveTimeout = 90000 @@ -160,6 +168,7 @@ export class BskyAppView { } async destroy(opts?: { skipDb: boolean }): Promise { + this.ctx.labelCache.stop() await this.ctx.didCache.destroy() await this.terminator?.terminate() await this.ctx.backgroundQueue.destroy() diff --git a/packages/bsky/src/indexer/services.ts b/packages/bsky/src/indexer/services.ts index 2cd2030287c..6bb06c0ffca 100644 --- a/packages/bsky/src/indexer/services.ts +++ b/packages/bsky/src/indexer/services.ts @@ -13,7 +13,7 @@ export function createServices(resources: { const { idResolver, labeler, backgroundQueue } = resources return { indexing: IndexingService.creator(idResolver, labeler, backgroundQueue), - label: LabelService.creator(), + label: LabelService.creator(null), } } diff --git a/packages/bsky/src/label-cache.ts b/packages/bsky/src/label-cache.ts new file mode 100644 index 00000000000..601152bee43 --- /dev/null +++ b/packages/bsky/src/label-cache.ts @@ -0,0 +1,90 @@ +import { wait } from '@atproto/common' +import Database from './db' +import { Label } from './db/tables/label' +import { labelerLogger as log } from './logger' + +export class LabelCache { + bySubject: Record = {} + latestLabel = '' + refreshes = 0 + + destroyed = false + + constructor(public db: Database) {} + + start() { + this.poll() + } + + async fullRefresh() { + const allLabels = await this.db.db.selectFrom('label').selectAll().execute() + this.wipeCache() + this.processLabels(allLabels) + } + + async partialRefresh() { + const labels = await this.db.db + .selectFrom('label') + .selectAll() + .where('cts', '>', this.latestLabel) + .execute() + this.processLabels(labels) + } + + async poll() { + try { + if (this.destroyed) return + if (this.refreshes >= 120) { + await this.fullRefresh() + this.refreshes = 0 + } else { + await this.partialRefresh() + this.refreshes++ + } + } catch (err) { + log.error( + { err, latestLabel: this.latestLabel, refreshes: this.refreshes }, + 'label cache failed to refresh', + ) + } + await wait(500) + this.poll() + } + + processLabels(labels: Label[]) { + for (const label of labels) { + if (label.cts > this.latestLabel) { + this.latestLabel = label.cts + } + this.bySubject[label.uri] ??= [] + this.bySubject[label.uri].push(label) + } + } + + wipeCache() { + this.bySubject = {} + } + + stop() { + this.destroyed = true + } + + forSubject(subject: string, includeNeg = false): Label[] { + const labels = this.bySubject[subject] ?? [] + return includeNeg ? labels : labels.filter((l) => l.neg === false) + } + + forSubjects(subjects: string[], includeNeg?: boolean): Label[] { + let labels: Label[] = [] + const alreadyAdded = new Set() + for (const subject of subjects) { + if (alreadyAdded.has(subject)) { + continue + } + const subLabels = this.forSubject(subject, includeNeg) + labels = [...labels, ...subLabels] + alreadyAdded.add(subject) + } + return labels + } +} diff --git a/packages/bsky/src/services/actor/index.ts b/packages/bsky/src/services/actor/index.ts index 3d053242bad..695af2e92bb 100644 --- a/packages/bsky/src/services/actor/index.ts +++ b/packages/bsky/src/services/actor/index.ts @@ -5,15 +5,20 @@ import { ActorViews } from './views' import { ImageUriBuilder } from '../../image/uri' import { Actor } from '../../db/tables/actor' import { TimeCidKeyset } from '../../db/pagination' +import { LabelCache } from '../../label-cache' export class ActorService { - constructor(public db: Database, public imgUriBuilder: ImageUriBuilder) {} + constructor( + public db: Database, + public imgUriBuilder: ImageUriBuilder, + public labelCache: LabelCache, + ) {} - static creator(imgUriBuilder: ImageUriBuilder) { - return (db: Database) => new ActorService(db, imgUriBuilder) + static creator(imgUriBuilder: ImageUriBuilder, labelCache: LabelCache) { + return (db: Database) => new ActorService(db, imgUriBuilder, labelCache) } - views = new ActorViews(this.db, this.imgUriBuilder) + views = new ActorViews(this.db, this.imgUriBuilder, this.labelCache) async getActorDid(handleOrDid: string): Promise { if (handleOrDid.startsWith('did:')) { diff --git a/packages/bsky/src/services/actor/views.ts b/packages/bsky/src/services/actor/views.ts index 7f0cc86ef03..f46f78ed39d 100644 --- a/packages/bsky/src/services/actor/views.ts +++ b/packages/bsky/src/services/actor/views.ts @@ -11,12 +11,17 @@ import { Actor } from '../../db/tables/actor' import { ImageUriBuilder } from '../../image/uri' import { LabelService } from '../label' import { GraphService } from '../graph' +import { LabelCache } from '../../label-cache' export class ActorViews { - constructor(private db: Database, private imgUriBuilder: ImageUriBuilder) {} + constructor( + private db: Database, + private imgUriBuilder: ImageUriBuilder, + private labelCache: LabelCache, + ) {} services = { - label: LabelService.creator()(this.db), + label: LabelService.creator(this.labelCache)(this.db), graph: GraphService.creator(this.imgUriBuilder)(this.db), } diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index bdd15f75c53..1b172443f8e 100644 --- a/packages/bsky/src/services/feed/index.ts +++ b/packages/bsky/src/services/feed/index.ts @@ -44,22 +44,27 @@ import { LabelService, Labels } from '../label' import { ActorService } from '../actor' import { GraphService } from '../graph' import { FeedViews } from './views' +import { LabelCache } from '../../label-cache' export * from './types' export class FeedService { - constructor(public db: Database, public imgUriBuilder: ImageUriBuilder) {} + constructor( + public db: Database, + public imgUriBuilder: ImageUriBuilder, + public labelCache: LabelCache, + ) {} views = new FeedViews(this.db, this.imgUriBuilder) services = { - label: LabelService.creator()(this.db), - actor: ActorService.creator(this.imgUriBuilder)(this.db), + label: LabelService.creator(this.labelCache)(this.db), + actor: ActorService.creator(this.imgUriBuilder, this.labelCache)(this.db), graph: GraphService.creator(this.imgUriBuilder)(this.db), } - static creator(imgUriBuilder: ImageUriBuilder) { - return (db: Database) => new FeedService(db, imgUriBuilder) + static creator(imgUriBuilder: ImageUriBuilder, labelCache: LabelCache) { + return (db: Database) => new FeedService(db, imgUriBuilder, labelCache) } selectPostQb() { diff --git a/packages/bsky/src/services/index.ts b/packages/bsky/src/services/index.ts index f8863986bbd..92c3e9b1f29 100644 --- a/packages/bsky/src/services/index.ts +++ b/packages/bsky/src/services/index.ts @@ -6,18 +6,20 @@ import { GraphService } from './graph' import { ModerationService } from './moderation' import { LabelService } from './label' import { ImageInvalidator } from '../image/invalidator' +import { LabelCache } from '../label-cache' export function createServices(resources: { imgUriBuilder: ImageUriBuilder imgInvalidator: ImageInvalidator + labelCache: LabelCache }): Services { - const { imgUriBuilder, imgInvalidator } = resources + const { imgUriBuilder, imgInvalidator, labelCache } = resources return { - actor: ActorService.creator(imgUriBuilder), - feed: FeedService.creator(imgUriBuilder), + actor: ActorService.creator(imgUriBuilder, labelCache), + feed: FeedService.creator(imgUriBuilder, labelCache), graph: GraphService.creator(imgUriBuilder), moderation: ModerationService.creator(imgUriBuilder, imgInvalidator), - label: LabelService.creator(), + label: LabelService.creator(labelCache), } } diff --git a/packages/bsky/src/services/label/index.ts b/packages/bsky/src/services/label/index.ts index b979799f126..c5090b346fc 100644 --- a/packages/bsky/src/services/label/index.ts +++ b/packages/bsky/src/services/label/index.ts @@ -3,14 +3,15 @@ import Database from '../../db' import { Label } from '../../lexicon/types/com/atproto/label/defs' import { ids } from '../../lexicon/lexicons' import { sql } from 'kysely' +import { LabelCache } from '../../label-cache' export type Labels = Record export class LabelService { - constructor(public db: Database) {} + constructor(public db: Database, public cache: LabelCache | null) {} - static creator() { - return (db: Database) => new LabelService(db) + static creator(cache: LabelCache | null) { + return (db: Database) => new LabelService(db, cache) } async formatAndCreate( @@ -62,15 +63,21 @@ export class LabelService { async getLabelsForUris( subjects: string[], - includeNeg?: boolean, + opts?: { + includeNeg?: boolean + skipCache?: boolean + }, ): Promise { if (subjects.length < 1) return {} - const res = await this.db.db - .selectFrom('label') - .where('label.uri', 'in', subjects) - .if(!includeNeg, (qb) => qb.where('neg', '=', false)) - .selectAll() - .execute() + const res = + this.cache === null || opts?.skipCache + ? await this.db.db + .selectFrom('label') + .where('label.uri', 'in', subjects) + .if(!opts?.includeNeg, (qb) => qb.where('neg', '=', false)) + .selectAll() + .execute() + : this.cache.forSubjects(subjects, opts?.includeNeg) return res.reduce((acc, cur) => { acc[cur.uri] ??= [] acc[cur.uri].push({ @@ -85,7 +92,10 @@ export class LabelService { // gets labels for any record. when did is present, combine labels for both did & profile record. async getLabelsForSubjects( subjects: string[], - includeNeg?: boolean, + opts?: { + includeNeg?: boolean + skipCache?: boolean + }, ): Promise { if (subjects.length < 1) return {} const expandedSubjects = subjects.flatMap((subject) => { @@ -97,7 +107,7 @@ export class LabelService { } return subject }) - const labels = await this.getLabelsForUris(expandedSubjects, includeNeg) + const labels = await this.getLabelsForUris(expandedSubjects, opts) return Object.keys(labels).reduce((acc, cur) => { const uri = cur.startsWith('at://') ? new AtUri(cur) : null if ( @@ -116,16 +126,25 @@ export class LabelService { }, {} as Labels) } - async getLabels(subject: string, includeNeg?: boolean): Promise { - const labels = await this.getLabelsForUris([subject], includeNeg) + async getLabels( + subject: string, + opts?: { + includeNeg?: boolean + skipCache?: boolean + }, + ): Promise { + const labels = await this.getLabelsForUris([subject], opts) return labels[subject] ?? [] } async getLabelsForProfile( did: string, - includeNeg?: boolean, + opts?: { + includeNeg?: boolean + skipCache?: boolean + }, ): Promise { - const labels = await this.getLabelsForSubjects([did], includeNeg) + const labels = await this.getLabelsForSubjects([did], opts) return labels[did] ?? [] } } diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index 67c5fb5067b..01f085789ae 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -126,6 +126,9 @@ export class TestBsky { await ingester.start() await indexer.start() await server.start() + + // we refresh label cache by hand in `processAll` instead of on a timer + server.ctx.labelCache.stop() return new TestBsky(url, port, server, indexer, ingester) } @@ -164,6 +167,7 @@ export class TestBsky { await Promise.all([ this.ctx.backgroundQueue.processAll(), this.indexer.ctx.backgroundQueue.processAll(), + this.ctx.labelCache.fullRefresh(), ]) } diff --git a/packages/pds/src/app-view/services/label/index.ts b/packages/pds/src/app-view/services/label/index.ts index aa00ca29f95..1975b5e07db 100644 --- a/packages/pds/src/app-view/services/label/index.ts +++ b/packages/pds/src/app-view/services/label/index.ts @@ -63,18 +63,20 @@ export class LabelService { async getLabelsForUris( subjects: string[], - includeNeg?: boolean, - skipCache?: boolean, + opts?: { + includeNeg?: boolean + skipCache?: boolean + }, ): Promise { if (subjects.length < 1) return {} - const res = skipCache + const res = opts?.skipCache ? await this.db.db .selectFrom('label') .where('label.uri', 'in', subjects) - .if(!includeNeg, (qb) => qb.where('neg', '=', 0)) + .if(!opts?.includeNeg, (qb) => qb.where('neg', '=', 0)) .selectAll() .execute() - : this.cache.forSubjects(subjects, includeNeg) + : this.cache.forSubjects(subjects, opts?.includeNeg) return res.reduce((acc, cur) => { acc[cur.uri] ??= [] acc[cur.uri].push({ @@ -89,8 +91,10 @@ export class LabelService { // gets labels for any record. when did is present, combine labels for both did & profile record. async getLabelsForSubjects( subjects: string[], - includeNeg?: boolean, - skipCache?: boolean, + opts?: { + includeNeg?: boolean + skipCache?: boolean + }, ): Promise { if (subjects.length < 1) return {} const expandedSubjects = subjects.flatMap((subject) => { @@ -102,11 +106,7 @@ export class LabelService { } return subject }) - const labels = await this.getLabelsForUris( - expandedSubjects, - includeNeg, - skipCache, - ) + const labels = await this.getLabelsForUris(expandedSubjects, opts) return Object.keys(labels).reduce((acc, cur) => { const uri = cur.startsWith('at://') ? new AtUri(cur) : null if ( @@ -127,18 +127,23 @@ export class LabelService { async getLabels( subject: string, - includeNeg?: boolean, - skipCache?: boolean, + opts?: { + includeNeg?: boolean + skipCache?: boolean + }, ): Promise { - const labels = await this.getLabelsForUris([subject], includeNeg, skipCache) + const labels = await this.getLabelsForUris([subject], opts) return labels[subject] ?? [] } async getLabelsForProfile( did: string, - includeNeg?: boolean, + opts?: { + includeNeg?: boolean + skipCache?: boolean + }, ): Promise { - const labels = await this.getLabelsForSubjects([did], includeNeg) + const labels = await this.getLabelsForSubjects([did], opts) return labels[did] ?? [] } } diff --git a/packages/pds/tests/proxied/admin.test.ts b/packages/pds/tests/proxied/admin.test.ts index d9c9b46b616..d02e531203f 100644 --- a/packages/pds/tests/proxied/admin.test.ts +++ b/packages/pds/tests/proxied/admin.test.ts @@ -285,7 +285,7 @@ describe('proxies admin requests', () => { ) const labelsA = await services.appView .label(db) - .getLabels(sc.dids.alice, false, true) + .getLabels(sc.dids.alice, { includeNeg: false, skipCache: true }) expect(labelsA.map((l) => l.val)).toEqual(['dogs']) // reverse action await agent.api.com.atproto.admin.reverseModerationAction( @@ -314,7 +314,7 @@ describe('proxies admin requests', () => { ) const labelsB = await services.appView .label(db) - .getLabels(sc.dids.alice, false, true) + .getLabels(sc.dids.alice, { includeNeg: false, skipCache: true }) expect(labelsB.map((l) => l.val)).toEqual(['cats']) }) @@ -356,7 +356,7 @@ describe('proxies admin requests', () => { await expect(tryGetPostAppview).rejects.toThrow(NotFoundError) const labelsA = await services.appView .label(db) - .getLabels(post.ref.uriStr, false, true) + .getLabels(post.ref.uriStr, { includeNeg: false, skipCache: true }) expect(labelsA.map((l) => l.val)).toEqual(['dogs']) // reverse action await agent.api.com.atproto.admin.reverseModerationAction( @@ -385,7 +385,7 @@ describe('proxies admin requests', () => { ) const labelsB = await services.appView .label(db) - .getLabels(post.ref.uriStr, false, true) + .getLabels(post.ref.uriStr, { includeNeg: false, skipCache: true }) expect(labelsB.map((l) => l.val)).toEqual(['cats']) }) From 3ed6dceafb0e20d7e210ec0a18188e7af2cfafdc Mon Sep 17 00:00:00 2001 From: bnewbold Date: Tue, 8 Aug 2023 15:56:38 -0700 Subject: [PATCH 114/237] remove unused/unspecified label (#1426) * labeler: remove redundant 'underwear' label This commit contains no change to mod policy, app behavior, etc. We have been creating redundant "underwear" labels along with "sexual" for many months. "sexual" is the actual label that gets actioned in mod preferences, etc. The un-specified "underwear" labels are confusing and take up resources on-disk and in API responses. * reflect in appview --------- Co-authored-by: dholms --- packages/bsky/src/labeler/hive.ts | 2 +- packages/pds/src/labeler/hive.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/bsky/src/labeler/hive.ts b/packages/bsky/src/labeler/hive.ts index e78539dfa1d..b40de2a2400 100644 --- a/packages/bsky/src/labeler/hive.ts +++ b/packages/bsky/src/labeler/hive.ts @@ -142,7 +142,7 @@ export const sexualLabels = (classes: HiveRespClass[]): string[] => { // TODO: retaining 'underwear' label for a short time to help understand // the impact of labeling all "underwear" as "sexual". This *will* be // pulling in somewhat non-sexual content in to "sexual" label. - return ['sexual', 'underwear'] + return ['sexual'] } } diff --git a/packages/pds/src/labeler/hive.ts b/packages/pds/src/labeler/hive.ts index bdbe70609bd..e4a0bc48045 100644 --- a/packages/pds/src/labeler/hive.ts +++ b/packages/pds/src/labeler/hive.ts @@ -138,10 +138,7 @@ export const sexualLabels = (classes: HiveRespClass[]): string[] => { // (after non-sexual content already labeled above) for (const nudityClass of ['yes_male_underwear', 'yes_female_underwear']) { if (scores[nudityClass] >= 0.9) { - // TODO: retaining 'underwear' label for a short time to help understand - // the impact of labeling all "underwear" as "sexual". This *will* be - // pulling in somewhat non-sexual content in to "sexual" label. - return ['sexual', 'underwear'] + return ['sexual'] } } From f9cce06c23d285d142e87b2f36ace25061968664 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 8 Aug 2023 16:18:03 -0700 Subject: [PATCH 115/237] Tune moderation downgrading on self-content to carry over blurring behaviors in some cases (#1453) * Tune moderation downgrading on self-content to carry over blurring behaviors in some cases * Fixes --- .../post-moderation-behaviors.json | 136 ++++++- .../api/docs/moderation-behaviors/posts.md | 334 +++++++++++++++++- packages/api/src/moderation/index.ts | 18 +- packages/api/src/moderation/util.ts | 14 +- 4 files changed, 484 insertions(+), 18 deletions(-) diff --git a/packages/api/definitions/post-moderation-behaviors.json b/packages/api/definitions/post-moderation-behaviors.json index e7aec1788c3..26df7863fbf 100644 --- a/packages/api/definitions/post-moderation-behaviors.json +++ b/packages/api/definitions/post-moderation-behaviors.json @@ -676,7 +676,7 @@ "author": "self", "labels": {"post": ["!hide"]}, "behaviors": { - "content": {"cause": "label:!hide", "alert": true} + "content": {"cause": "label:!hide", "blur": true} } }, "Self-post: Imperative label ('!hide') on author profile": { @@ -700,7 +700,7 @@ "quoteAuthor": "self", "labels": {"quotedPost": ["!hide"]}, "behaviors": { - "embed": {"cause": "label:!hide", "alert": true} + "embed": {"cause": "label:!hide", "blur": true} } }, "Self-post: Imperative label ('!hide') on quoted author account": { @@ -712,6 +712,138 @@ "behaviors": {} }, + "Self-post: Imperative label ('!warn') on post": { + "cfg": "none", + "subject": "post", + "author": "self", + "labels": {"post": ["!warn"]}, + "behaviors": { + "content": {"cause": "label:!warn", "blur": true} + } + }, + "Self-post: Imperative label ('!warn') on author profile": { + "cfg": "none", + "subject": "post", + "author": "self", + "labels": {"profile": ["!warn"]}, + "behaviors": {} + }, + "Self-post: Imperative label ('!warn') on author account": { + "cfg": "none", + "subject": "post", + "author": "self", + "labels": {"account": ["!warn"]}, + "behaviors": {} + }, + "Self-post: Imperative label ('!warn') on quoted post": { + "cfg": "none", + "subject": "post", + "author": "self", + "quoteAuthor": "self", + "labels": {"quotedPost": ["!warn"]}, + "behaviors": { + "embed": {"cause": "label:!warn", "blur": true} + } + }, + "Self-post: Imperative label ('!warn') on quoted author account": { + "cfg": "none", + "subject": "post", + "author": "self", + "quoteAuthor": "self", + "labels": {"quotedAccount": ["!warn"]}, + "behaviors": {} + }, + + "Self-post: Blur-media label ('porn') on post (hide)": { + "cfg": "porn-hide", + "subject": "post", + "author": "self", + "labels": {"post": ["porn"]}, + "behaviors": { + "embed": {"cause": "label:porn", "blur": true} + } + }, + "Self-post: Blur-media label ('porn') on author profile (hide)": { + "cfg": "porn-hide", + "subject": "post", + "author": "self", + "labels": {"profile": ["porn"]}, + "behaviors": { + } + }, + "Self-post: Blur-media label ('porn') on author account (hide)": { + "cfg": "porn-hide", + "subject": "post", + "author": "self", + "labels": {"account": ["porn"]}, + "behaviors": { + } + }, + "Self-post: Blur-media label ('porn') on quoted post (hide)": { + "cfg": "porn-hide", + "subject": "post", + "author": "self", + "quoteAuthor": "self", + "labels": {"quotedPost": ["porn"]}, + "behaviors": { + "embed": {"cause": "label:porn", "blur": true} + } + }, + "Self-post: Blur-media label ('porn') on quoted author account (hide)": { + "cfg": "porn-hide", + "subject": "post", + "author": "self", + "quoteAuthor": "self", + "labels": {"quotedAccount": ["porn"]}, + "behaviors": { + } + }, + + "Self-post: Blur-media label ('porn') on post (warn)": { + "cfg": "porn-warn", + "subject": "post", + "author": "self", + "labels": {"post": ["porn"]}, + "behaviors": { + "embed": {"cause": "label:porn", "blur": true} + } + }, + "Self-post: Blur-media label ('porn') on author profile (warn)": { + "cfg": "porn-warn", + "subject": "post", + "author": "self", + "labels": {"profile": ["porn"]}, + "behaviors": { + } + }, + "Self-post: Blur-media label ('porn') on author account (warn)": { + "cfg": "porn-warn", + "subject": "post", + "author": "self", + "labels": {"account": ["porn"]}, + "behaviors": { + } + }, + "Self-post: Blur-media label ('porn') on quoted post (warn)": { + "cfg": "porn-warn", + "subject": "post", + "author": "self", + "quoteAuthor": "self", + "labels": {"quotedPost": ["porn"]}, + "behaviors": { + "embed": {"cause": "label:porn", "blur": true} + } + }, + "Self-post: Blur-media label ('porn') on quoted author account (warn)": { + "cfg": "porn-warn", + "subject": "post", + "author": "self", + "quoteAuthor": "self", + "labels": {"quotedAccount": ["porn"]}, + "behaviors": { + } + }, + "Post with blocked author": { "cfg": "none", "subject": "post", diff --git a/packages/api/docs/moderation-behaviors/posts.md b/packages/api/docs/moderation-behaviors/posts.md index 9b77bbc73bb..ef3c6c7fc5d 100644 --- a/packages/api/docs/moderation-behaviors/posts.md +++ b/packages/api/docs/moderation-behaviors/posts.md @@ -1463,8 +1463,8 @@ Key: +✋ -🪧 @@ -1537,8 +1537,8 @@ Key: +✋ -🪧 @@ -1566,6 +1566,336 @@ Key: + + +Self-post: Imperative label ('!warn') on post + + + + +✋ + + + + + + + + + + + + + + + + +Self-post: Imperative label ('!warn') on author profile + + + + + + + + + + + + + + + + + + + + + +Self-post: Imperative label ('!warn') on author account + + + + + + + + + + + + + + + + + + + + + +Self-post: Imperative label ('!warn') on quoted post + + + + + + + + + + + + +✋ + + + + + + + + +Self-post: Imperative label ('!warn') on quoted author account + + + + + + + + + + + + + + + + + + + + + +Self-post: Blur-media label ('porn') on post (hide) + + + + + + + + + + + + +✋ + + + + + + + + +Self-post: Blur-media label ('porn') on author profile (hide) + + + + + + + + + + + + + + + + + + + + + +Self-post: Blur-media label ('porn') on author account (hide) + + + + + + + + + + + + + + + + + + + + + +Self-post: Blur-media label ('porn') on quoted post (hide) + + + + + + + + + + + + +✋ + + + + + + + + +Self-post: Blur-media label ('porn') on quoted author account (hide) + + + + + + + + + + + + + + + + + + + + + +Self-post: Blur-media label ('porn') on post (warn) + + + + + + + + + + + + +✋ + + + + + + + + +Self-post: Blur-media label ('porn') on author profile (warn) + + + + + + + + + + + + + + + + + + + + + +Self-post: Blur-media label ('porn') on author account (warn) + + + + + + + + + + + + + + + + + + + + + +Self-post: Blur-media label ('porn') on quoted post (warn) + + + + + + + + + + + + +✋ + + + + + + + + +Self-post: Blur-media label ('porn') on quoted author account (warn) + + + + + + + + + + + + + + + + + + + ScenarioFilterContentAvatarEmbed Post with blocked author diff --git a/packages/api/src/moderation/index.ts b/packages/api/src/moderation/index.ts index d659783b67a..a5f62ab33e4 100644 --- a/packages/api/src/moderation/index.ts +++ b/packages/api/src/moderation/index.ts @@ -61,10 +61,10 @@ export function moderateProfile( // downgrade based on authorship if (!isModerationDecisionNoop(account) && account.did === opts.userDid) { - downgradeDecision(account, { alert: true }) + downgradeDecision(account, 'alert') } if (!isModerationDecisionNoop(profile) && profile.did === opts.userDid) { - downgradeDecision(profile, { alert: true }) + downgradeDecision(profile, 'alert') } // derive avatar blurring from account & profile, but override for mutes because that shouldnt blur @@ -154,23 +154,23 @@ export function moderatePost( // downgrade based on authorship if (!isModerationDecisionNoop(post) && post.did === opts.userDid) { - downgradeDecision(post, { alert: true }) + downgradeDecision(post, 'blur') } - if (!isModerationDecisionNoop(account) && account.did === opts.userDid) { - downgradeDecision(account, { alert: false }) + if (account.cause && account.did === opts.userDid) { + downgradeDecision(account, 'noop') } - if (!isModerationDecisionNoop(profile) && profile.did === opts.userDid) { - downgradeDecision(profile, { alert: false }) + if (profile.cause && profile.did === opts.userDid) { + downgradeDecision(profile, 'noop') } if (quote && !isModerationDecisionNoop(quote) && quote.did === opts.userDid) { - downgradeDecision(quote, { alert: true }) + downgradeDecision(quote, 'blur') } if ( quotedAccount && !isModerationDecisionNoop(quotedAccount) && quotedAccount.did === opts.userDid ) { - downgradeDecision(quotedAccount, { alert: false }) + downgradeDecision(quotedAccount, 'noop') } // derive filtering from feeds from the post, post author's account, diff --git a/packages/api/src/moderation/util.ts b/packages/api/src/moderation/util.ts index 56330c2a458..f1f6b9f07f7 100644 --- a/packages/api/src/moderation/util.ts +++ b/packages/api/src/moderation/util.ts @@ -34,15 +34,19 @@ export function takeHighestPriorityDecision( export function downgradeDecision( decision: ModerationDecision, - { alert }: { alert: boolean }, + to: 'blur' | 'alert' | 'noop', ) { - decision.blur = false - decision.blurMedia = false decision.filter = false decision.noOverride = false - decision.alert = alert - if (!alert) { + if (to === 'noop') { + decision.blur = false + decision.blurMedia = false + decision.alert = false delete decision.cause + } else if (to === 'alert') { + decision.blur = false + decision.blurMedia = false + decision.alert = true } } From 6dbf4c67bc048cd329055cd619a1535bd983dec2 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 8 Aug 2023 16:18:38 -0700 Subject: [PATCH 116/237] @atproto/api@0.5.3 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 0d568647695..2d837091606 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.5.2", + "version": "0.5.3", "main": "src/index.ts", "scripts": { "codegen": "yarn docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From 0f2c006641acec505366e1f8d351e10115c7b57f Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 8 Aug 2023 16:22:54 -0700 Subject: [PATCH 117/237] @atproto/api@0.5.4 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 2d837091606..7b4cbafa92e 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.5.3", + "version": "0.5.4", "main": "src/index.ts", "scripts": { "codegen": "yarn docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From e1d10d21c004a1323a426bbe84674b66327a439c Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 8 Aug 2023 19:16:00 -0500 Subject: [PATCH 118/237] Increase backfill page size (#1432) increase backfill page size --- packages/pds/src/sequencer/outbox.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/src/sequencer/outbox.ts b/packages/pds/src/sequencer/outbox.ts index d56e6bd0fb6..d248099138c 100644 --- a/packages/pds/src/sequencer/outbox.ts +++ b/packages/pds/src/sequencer/outbox.ts @@ -104,7 +104,7 @@ export class Outbox { // yields only historical events async *getBackfill(backfillCursor: number, backfillTime?: string) { - const PAGE_SIZE = 200 + const PAGE_SIZE = 500 while (true) { const evts = await this.sequencer.requestSeqRange({ earliestTime: backfillTime, From 203e72f1c1b49eb5fd291ae6b30dafe84c451769 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Wed, 9 Aug 2023 10:21:43 -0400 Subject: [PATCH 119/237] Appview preset image URLs (#1248) * update image signing logic to presets on bsky * update bsky config and views for preset img urls * update bsky image tests for preset urls * update bsky snapshots for preset image urls * update pds proxy tests for bsky preset image urls * tweak image paths --- .../bsky/src/api/app/bsky/graph/getList.ts | 2 +- packages/bsky/src/config.ts | 17 -- packages/bsky/src/image/invalidator.ts | 9 +- packages/bsky/src/image/server.ts | 13 +- packages/bsky/src/image/uri.ts | 238 +++++------------- packages/bsky/src/index.ts | 6 +- packages/bsky/src/services/actor/views.ts | 18 +- packages/bsky/src/services/feed/index.ts | 6 +- packages/bsky/src/services/feed/views.ts | 12 +- packages/bsky/src/services/graph/index.ts | 4 +- .../bsky/src/services/moderation/index.ts | 8 +- .../feed-generation.test.ts.snap | 64 ++--- .../tests/__snapshots__/indexing.test.ts.snap | 12 +- packages/bsky/tests/_util.ts | 11 +- packages/bsky/tests/image/server.test.ts | 61 ++--- packages/bsky/tests/image/uri.test.ts | 228 ++++------------- packages/bsky/tests/moderation.test.ts | 2 +- .../__snapshots__/actor-search.test.ts.snap | 20 +- .../__snapshots__/author-feed.test.ts.snap | 82 +++--- .../views/__snapshots__/blocks.test.ts.snap | 12 +- .../views/__snapshots__/follows.test.ts.snap | 68 ++--- .../views/__snapshots__/likes.test.ts.snap | 2 +- .../__snapshots__/mute-lists.test.ts.snap | 38 +-- .../views/__snapshots__/mutes.test.ts.snap | 20 +- .../__snapshots__/notifications.test.ts.snap | 20 +- .../views/__snapshots__/posts.test.ts.snap | 28 +-- .../views/__snapshots__/profile.test.ts.snap | 12 +- .../views/__snapshots__/reposts.test.ts.snap | 2 +- .../views/__snapshots__/thread.test.ts.snap | 60 ++--- .../views/__snapshots__/timeline.test.ts.snap | 224 ++++++++--------- packages/dev-env/src/bsky.ts | 3 - packages/pds/tests/_util.ts | 11 +- .../__snapshots__/feedgen.test.ts.snap | 44 ++-- .../proxied/__snapshots__/views.test.ts.snap | 134 +++++----- 34 files changed, 591 insertions(+), 900 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/graph/getList.ts b/packages/bsky/src/api/app/bsky/graph/getList.ts index 1a12baa52ca..ee0e9dc704d 100644 --- a/packages/bsky/src/api/app/bsky/graph/getList.ts +++ b/packages/bsky/src/api/app/bsky/graph/getList.ts @@ -62,7 +62,7 @@ export default function (server: Server, ctx: AppContext) { ? JSON.parse(listRes.descriptionFacets) : undefined, avatar: listRes.avatarCid - ? ctx.imgUriBuilder.getCommonSignedUri( + ? ctx.imgUriBuilder.getPresetUri( 'avatar', listRes.creator, listRes.avatarCid, diff --git a/packages/bsky/src/config.ts b/packages/bsky/src/config.ts index 560f9cc02a5..f5f48aa3445 100644 --- a/packages/bsky/src/config.ts +++ b/packages/bsky/src/config.ts @@ -13,8 +13,6 @@ export interface ServerConfigValues { didPlcUrl: string didCacheStaleTTL: number didCacheMaxTTL: number - imgUriSalt: string - imgUriKey: string imgUriEndpoint?: string blobCacheLocation?: string labelerDid: string @@ -44,11 +42,6 @@ export class ServerConfig { process.env.DID_CACHE_MAX_TTL, DAY, ) - const imgUriSalt = - process.env.IMG_URI_SALT || '9dd04221f5755bce5f55f47464c27e1e' - const imgUriKey = - process.env.IMG_URI_KEY || - 'f23ecd142835025f42c3db2cf25dd813956c178392760256211f9d315f8ab4d8' const imgUriEndpoint = process.env.IMG_URI_ENDPOINT const blobCacheLocation = process.env.BLOB_CACHE_LOC const dbPostgresUrl = @@ -71,8 +64,6 @@ export class ServerConfig { didPlcUrl, didCacheStaleTTL, didCacheMaxTTL, - imgUriSalt, - imgUriKey, imgUriEndpoint, blobCacheLocation, labelerDid, @@ -140,14 +131,6 @@ export class ServerConfig { return this.cfg.didPlcUrl } - get imgUriSalt() { - return this.cfg.imgUriSalt - } - - get imgUriKey() { - return this.cfg.imgUriKey - } - get imgUriEndpoint() { return this.cfg.imgUriEndpoint } diff --git a/packages/bsky/src/image/invalidator.ts b/packages/bsky/src/image/invalidator.ts index d1319951500..70bf363371d 100644 --- a/packages/bsky/src/image/invalidator.ts +++ b/packages/bsky/src/image/invalidator.ts @@ -1,4 +1,5 @@ import { BlobCache } from './server' +import { ImageUriBuilder } from './uri' // Invalidation is a general interface for propagating an image blob // takedown through any caches where a representation of it may be stored. @@ -15,7 +16,13 @@ export class ImageProcessingServerInvalidator implements ImageInvalidator { paths.map(async (path) => { const [, signature] = path.split('/') if (!signature) throw new Error('Missing signature') - await this.cache.clear(signature) + const options = ImageUriBuilder.getOptions(path) + const cacheKey = [ + options.did, + options.cid.toString(), + options.preset, + ].join('::') + await this.cache.clear(cacheKey) }), ) const rejection = results.find( diff --git a/packages/bsky/src/image/server.ts b/packages/bsky/src/image/server.ts index d4509aa8e80..563d9b5bf4b 100644 --- a/packages/bsky/src/image/server.ts +++ b/packages/bsky/src/image/server.ts @@ -24,7 +24,7 @@ export class ImageProcessingServer { uriBuilder: ImageUriBuilder constructor(public cfg: ServerConfig, public cache: BlobCache) { - this.uriBuilder = new ImageUriBuilder('', cfg.imgUriSalt, cfg.imgUriKey) + this.uriBuilder = new ImageUriBuilder('') this.app.get('*', this.handler.bind(this)) this.app.use(errorMiddleware) } @@ -36,12 +36,17 @@ export class ImageProcessingServer { ) { try { const path = req.path - const options = this.uriBuilder.getVerifiedOptions(path) + const options = ImageUriBuilder.getOptions(path) + const cacheKey = [ + options.did, + options.cid.toString(), + options.preset, + ].join('::') // Cached flow try { - const cachedImage = await this.cache.get(options.signature) + const cachedImage = await this.cache.get(cacheKey) res.statusCode = 200 res.setHeader('x-cache', 'hit') res.setHeader('content-type', getMime(options.format)) @@ -69,7 +74,7 @@ export class ImageProcessingServer { // Cache in the background this.cache - .put(options.signature, cloneStream(processedImage)) + .put(cacheKey, cloneStream(processedImage)) .catch((err) => log.error(err, 'failed to cache image')) // Respond res.statusCode = 200 diff --git a/packages/bsky/src/image/uri.ts b/packages/bsky/src/image/uri.ts index 223b2be6f24..5e288e29d10 100644 --- a/packages/bsky/src/image/uri.ts +++ b/packages/bsky/src/image/uri.ts @@ -1,199 +1,67 @@ -import { createHmac } from 'crypto' -import * as uint8arrays from 'uint8arrays' import { CID } from 'multiformats/cid' import { Options } from './util' -// @NOTE if there are any additions here, ensure to include them on ImageUriBuilder.commonSignedUris -type CommonSignedUris = 'avatar' | 'banner' | 'feed_thumbnail' | 'feed_fullsize' +// @NOTE if there are any additions here, ensure to include them on ImageUriBuilder.presets +export type ImagePreset = + | 'avatar' + | 'banner' + | 'feed_thumbnail' + | 'feed_fullsize' -const PATH_REGEX = /^\/(.+)\/plain\/(.+?)\/(.+?)@(.+)$/ +const PATH_REGEX = /^\/(.+?)\/plain\/(.+?)\/(.+?)@(.+?)$/ export class ImageUriBuilder { - public endpoint: string - private salt: Uint8Array - private key: Uint8Array + constructor(public endpoint: string) {} - constructor( - endpoint: string, - salt: Uint8Array | string, - key: Uint8Array | string, - ) { - this.endpoint = endpoint - this.salt = - typeof salt === 'string' ? uint8arrays.fromString(salt, 'hex') : salt - this.key = - typeof key === 'string' ? uint8arrays.fromString(key, 'hex') : key - } - - getSignedPath(opts: Options & BlobLocation): string { - const path = ImageUriBuilder.getPath(opts) - const saltedPath = uint8arrays.concat([ - this.salt, - uint8arrays.fromString(path), - ]) - const sig = hmac(this.key, saltedPath).toString('base64url') - return `/${sig}${path}` - } - - getSignedUri(opts: Options & BlobLocation): string { - const path = this.getSignedPath(opts) - return this.endpoint + path - } - - static commonSignedUris: CommonSignedUris[] = [ + static presets: ImagePreset[] = [ 'avatar', 'banner', 'feed_thumbnail', 'feed_fullsize', ] - getCommonSignedUri( - id: CommonSignedUris, - did: string, - cid: string | CID, - ): string { - if (id === 'avatar') { - return this.getSignedUri({ - did, - cid: typeof cid === 'string' ? CID.parse(cid) : cid, - format: 'jpeg', - fit: 'cover', - height: 1000, - width: 1000, - min: true, - }) - } else if (id === 'banner') { - return this.getSignedUri({ - did, - cid: typeof cid === 'string' ? CID.parse(cid) : cid, - format: 'jpeg', - fit: 'cover', - height: 1000, - width: 3000, - min: true, - }) - } else if (id === 'feed_fullsize') { - return this.getSignedUri({ - did, - cid: typeof cid === 'string' ? CID.parse(cid) : cid, - format: 'jpeg', - fit: 'inside', - height: 2000, - width: 2000, - min: true, - }) - } else if (id === 'feed_thumbnail') { - return this.getSignedUri({ + getPresetUri(id: ImagePreset, did: string, cid: string | CID): string { + const options = presets[id] + if (!options) { + throw new Error(`Unrecognized requested common uri type: ${id}`) + } + return ( + this.endpoint + + ImageUriBuilder.getPath({ + preset: id, did, cid: typeof cid === 'string' ? CID.parse(cid) : cid, - format: 'jpeg', - fit: 'inside', - height: 1000, - width: 1000, - min: true, }) - } else { - const exhaustiveCheck: never = id - throw new Error( - `Unrecognized requested common uri type: ${exhaustiveCheck}`, - ) - } - } - - getVerifiedOptions( - path: string, - ): Options & BlobLocation & { signature: string } { - if (path.at(0) !== '/') { - throw new BadPathError('Invalid path: does not start with a slash') - } - const pathParts = path.split('/') // ['', sig, 'rs:fill:...', ...] - const [sig] = pathParts.splice(1, 1) // ['', 'rs:fill:...', ...] - const unsignedPath = pathParts.join('/') - if (!sig || sig.includes(':')) { - throw new BadPathError('Invalid path: missing signature') - } - const saltedPath = uint8arrays.concat([ - this.salt, - uint8arrays.fromString(unsignedPath), - ]) - const validSig = hmac(this.key, saltedPath).toString('base64url') - if (sig !== validSig) { - throw new BadPathError('Invalid path: bad signature') - } - const options = ImageUriBuilder.getOptions(unsignedPath) - return { - signature: validSig, - ...options, - } + ) } - static getPath(opts: Options & BlobLocation) { - const fit = opts.fit === 'inside' ? 'fit' : 'fill' // fit default is 'cover' - const enlarge = opts.min === true ? 1 : 0 // min default is false - const resize = `rs:${fit}:${opts.width}:${opts.height}:${enlarge}:0` // final ':0' is for interop with imgproxy - const minWidth = - opts.min && typeof opts.min === 'object' ? `mw:${opts.min.width}` : null - const minHeight = - opts.min && typeof opts.min === 'object' ? `mh:${opts.min.height}` : null - const quality = opts.quality ? `q:${opts.quality}` : null - return ( - `/` + - [resize, minWidth, minHeight, quality].filter(Boolean).join('/') + - `/plain/${opts.did}/${opts.cid.toString()}@${opts.format}` - ) + static getPath(opts: { preset: ImagePreset } & BlobLocation) { + const { format } = presets[opts.preset] + return `/${opts.preset}/plain/${opts.did}/${opts.cid.toString()}@${format}` } - static getOptions(path: string): Options & BlobLocation { + static getOptions( + path: string, + ): Options & BlobLocation & { preset: ImagePreset } { const match = path.match(PATH_REGEX) if (!match) { throw new BadPathError('Invalid path') } - const [, partsStr, did, cid, format] = match - if (format !== 'png' && format !== 'jpeg') { - throw new BadPathError('Invalid path: bad format') - } - const parts = partsStr.split('/') - const resizePart = parts.find((part) => part.startsWith('rs:')) - const qualityPart = parts.find((part) => part.startsWith('q:')) - const minWidthPart = parts.find((part) => part.startsWith('mw:')) - const minHeightPart = parts.find((part) => part.startsWith('mh:')) - const [, fit, width, height, enlarge] = resizePart?.split(':') ?? [] - const [, quality] = qualityPart?.split(':') ?? [] - const [, minWidth] = minWidthPart?.split(':') ?? [] - const [, minHeight] = minHeightPart?.split(':') ?? [] - if (fit !== 'fill' && fit !== 'fit') { - throw new BadPathError('Invalid path: bad resize fit param') - } - if (isNaN(toInt(width)) || isNaN(toInt(height))) { - throw new BadPathError('Invalid path: bad resize height/width param') - } - if (enlarge !== '0' && enlarge !== '1') { - throw new BadPathError('Invalid path: bad resize enlarge param') - } - if (quality && isNaN(toInt(quality))) { - throw new BadPathError('Invalid path: bad quality param') + const [, presetUnsafe, did, cid, formatUnsafe] = match + if (!(ImageUriBuilder.presets as string[]).includes(presetUnsafe)) { + throw new BadPathError('Invalid path: bad preset') } - if ( - (!minWidth && minHeight) || - (minWidth && !minHeight) || - (minWidth && isNaN(toInt(minWidth))) || - (minHeight && isNaN(toInt(minHeight))) || - (enlarge === '1' && (minHeight || minHeight)) - ) { - throw new BadPathError('Invalid path: bad min width/height param') + if (formatUnsafe !== 'jpeg' && formatUnsafe !== 'png') { + throw new BadPathError('Invalid path: bad format') } + const preset = presetUnsafe as ImagePreset + const format = formatUnsafe as Options['format'] return { + ...presets[preset], did, cid: CID.parse(cid), + preset, format, - height: toInt(height), - width: toInt(width), - fit: fit === 'fill' ? 'cover' : 'inside', - quality: quality ? toInt(quality) : undefined, - min: - minWidth && minHeight - ? { width: toInt(minWidth), height: toInt(minHeight) } - : enlarge === '1', } } } @@ -202,13 +70,33 @@ type BlobLocation = { cid: CID; did: string } export class BadPathError extends Error {} -function toInt(str: string) { - if (!/^\d+$/.test(str)) { - return NaN // String must be all numeric - } - return parseInt(str, 10) -} - -function hmac(key: Uint8Array, message: Uint8Array) { - return createHmac('sha256', key).update(message).digest() +export const presets: Record = { + avatar: { + format: 'jpeg', + fit: 'cover', + height: 1000, + width: 1000, + min: true, + }, + banner: { + format: 'jpeg', + fit: 'cover', + height: 1000, + width: 3000, + min: true, + }, + feed_thumbnail: { + format: 'jpeg', + fit: 'inside', + height: 2000, + width: 2000, + min: true, + }, + feed_fullsize: { + format: 'jpeg', + fit: 'inside', + height: 1000, + width: 1000, + min: true, + }, } diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 2bb115729b2..ae96ec18a1a 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -69,9 +69,7 @@ export class BskyAppView { const idResolver = new IdResolver({ plcUrl: config.didPlcUrl, didCache }) const imgUriBuilder = new ImageUriBuilder( - config.imgUriEndpoint || `${config.publicUrl}/image`, - config.imgUriSalt, - config.imgUriKey, + config.imgUriEndpoint || `${config.publicUrl}/img`, ) let imgProcessingServer: ImageProcessingServer | undefined @@ -128,7 +126,7 @@ export class BskyAppView { app.use(health.createRouter(ctx)) app.use(blobResolver.createRouter(ctx)) if (imgProcessingServer) { - app.use('/image', imgProcessingServer.app) + app.use('/img', imgProcessingServer.app) } app.use(server.xrpc.router) app.use(error.handler) diff --git a/packages/bsky/src/services/actor/views.ts b/packages/bsky/src/services/actor/views.ts index f46f78ed39d..be08c0a3328 100644 --- a/packages/bsky/src/services/actor/views.ts +++ b/packages/bsky/src/services/actor/views.ts @@ -115,18 +115,10 @@ export class ActorViews { return profileInfos.reduce((acc, cur) => { const actorLabels = labels[cur.did] ?? [] const avatar = cur?.avatarCid - ? this.imgUriBuilder.getCommonSignedUri( - 'avatar', - cur.did, - cur.avatarCid, - ) + ? this.imgUriBuilder.getPresetUri('avatar', cur.did, cur.avatarCid) : undefined const banner = cur?.bannerCid - ? this.imgUriBuilder.getCommonSignedUri( - 'banner', - cur.did, - cur.bannerCid, - ) + ? this.imgUriBuilder.getPresetUri('banner', cur.did, cur.bannerCid) : undefined const mutedByList = cur.requesterMutedByList && listViews[cur.requesterMutedByList] @@ -265,11 +257,7 @@ export class ActorViews { return profileInfos.reduce((acc, cur) => { const actorLabels = labels[cur.did] ?? [] const avatar = cur?.avatarCid - ? this.imgUriBuilder.getCommonSignedUri( - 'avatar', - cur.did, - cur.avatarCid, - ) + ? this.imgUriBuilder.getPresetUri('avatar', cur.did, cur.avatarCid) : undefined const mutedByList = cur.requesterMutedByList && listViews[cur.requesterMutedByList] diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index 1b172443f8e..449ff8d7e88 100644 --- a/packages/bsky/src/services/feed/index.ts +++ b/packages/bsky/src/services/feed/index.ts @@ -200,11 +200,7 @@ export class FeedService { return actors.reduce((acc, cur) => { const actorLabels = labels[cur.did] ?? [] const avatar = cur.avatarCid - ? this.imgUriBuilder.getCommonSignedUri( - 'avatar', - cur.did, - cur.avatarCid, - ) + ? this.imgUriBuilder.getPresetUri('avatar', cur.did, cur.avatarCid) : undefined const mutedByList = cur.requesterMutedByList && listViews[cur.requesterMutedByList] diff --git a/packages/bsky/src/services/feed/views.ts b/packages/bsky/src/services/feed/views.ts index fe986fe9a54..d366ab0d3d3 100644 --- a/packages/bsky/src/services/feed/views.ts +++ b/packages/bsky/src/services/feed/views.ts @@ -63,7 +63,7 @@ export class FeedViews { ? JSON.parse(info.descriptionFacets) : undefined, avatar: info.avatarCid - ? this.imgUriBuilder.getCommonSignedUri( + ? this.imgUriBuilder.getPresetUri( 'avatar', info.creator, info.avatarCid, @@ -228,12 +228,12 @@ export class FeedViews { imagesEmbedView(did: string, embed: EmbedImages) { const imgViews = embed.images.map((img) => ({ - thumb: this.imgUriBuilder.getCommonSignedUri( + thumb: this.imgUriBuilder.getPresetUri( 'feed_thumbnail', did, img.image.ref, ), - fullsize: this.imgUriBuilder.getCommonSignedUri( + fullsize: this.imgUriBuilder.getPresetUri( 'feed_fullsize', did, img.image.ref, @@ -255,11 +255,7 @@ export class FeedViews { title, description, thumb: thumb - ? this.imgUriBuilder.getCommonSignedUri( - 'feed_thumbnail', - did, - thumb.ref, - ) + ? this.imgUriBuilder.getPresetUri('feed_thumbnail', did, thumb.ref) : undefined, }, } diff --git a/packages/bsky/src/services/graph/index.ts b/packages/bsky/src/services/graph/index.ts index 52834a500ac..a33c574ecc0 100644 --- a/packages/bsky/src/services/graph/index.ts +++ b/packages/bsky/src/services/graph/index.ts @@ -218,7 +218,7 @@ export class GraphService { ? JSON.parse(list.descriptionFacets) : undefined, avatar: list.avatarCid - ? this.imgUriBuilder.getCommonSignedUri( + ? this.imgUriBuilder.getPresetUri( 'avatar', list.creator, list.avatarCid, @@ -238,7 +238,7 @@ export class GraphService { name: list.name, purpose: list.purpose, avatar: list.avatarCid - ? this.imgUriBuilder.getCommonSignedUri( + ? this.imgUriBuilder.getPresetUri( 'avatar', list.creator, list.avatarCid, diff --git a/packages/bsky/src/services/moderation/index.ts b/packages/bsky/src/services/moderation/index.ts index 4958b9ec8c5..4f5b2b8d326 100644 --- a/packages/bsky/src/services/moderation/index.ts +++ b/packages/bsky/src/services/moderation/index.ts @@ -388,12 +388,8 @@ export class ModerationService { if (info.blobCids) { await Promise.all( info.blobCids.map(async (cid) => { - const paths = ImageUriBuilder.commonSignedUris.map((id) => { - const uri = this.imgUriBuilder.getCommonSignedUri( - id, - info.uri.host, - cid, - ) + const paths = ImageUriBuilder.presets.map((id) => { + const uri = this.imgUriBuilder.getPresetUri(id, info.uri.host, cid) return uri.replace(this.imgUriBuilder.endpoint, '') }) await this.imgInvalidator.invalidate(cid.toString(), paths) diff --git a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap index 92d6e4e8a6c..a3b4e35a342 100644 --- a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap +++ b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap @@ -3,7 +3,7 @@ exports[`feed generation does not embed taken-down feed generator records in posts 1`] = ` Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "bobby", "handle": "bob.test", @@ -46,7 +46,7 @@ Object { exports[`feed generation embeds feed generator records in posts 1`] = ` Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "bobby", "handle": "bob.test", @@ -63,7 +63,7 @@ Object { "$type": "app.bsky.feed.defs#generatorView", "cid": "cids(2)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", "did": "user(3)", "displayName": "ali", "handle": "alice.test", @@ -113,7 +113,7 @@ Array [ Object { "cid": "cids(0)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", "description": "its me!", "did": "user(1)", "displayName": "ali", @@ -138,7 +138,7 @@ Array [ Object { "cid": "cids(2)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", "description": "its me!", "did": "user(1)", "displayName": "ali", @@ -163,7 +163,7 @@ Array [ Object { "cid": "cids(3)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", "description": "its me!", "did": "user(1)", "displayName": "ali", @@ -188,7 +188,7 @@ Array [ Object { "cid": "cids(4)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", "description": "its me!", "did": "user(1)", "displayName": "ali", @@ -220,7 +220,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -248,7 +248,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -300,13 +300,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(4)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(4)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(5)@jpeg", + "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", }, ], }, @@ -314,7 +314,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -436,7 +436,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -463,7 +463,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -526,13 +526,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(4)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(4)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(5)@jpeg", + "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", }, ], }, @@ -540,7 +540,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -673,7 +673,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -719,13 +719,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(3)@jpeg", + "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", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(4)@jpeg", + "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", }, ], }, @@ -733,7 +733,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -855,7 +855,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -882,7 +882,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -918,7 +918,7 @@ Object { "view": Object { "cid": "cids(0)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -949,7 +949,7 @@ Object { Object { "cid": "cids(0)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -974,7 +974,7 @@ Object { Object { "cid": "cids(2)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", diff --git a/packages/bsky/tests/__snapshots__/indexing.test.ts.snap b/packages/bsky/tests/__snapshots__/indexing.test.ts.snap index a3d11a3fae7..6f48e290911 100644 --- a/packages/bsky/tests/__snapshots__/indexing.test.ts.snap +++ b/packages/bsky/tests/__snapshots__/indexing.test.ts.snap @@ -84,7 +84,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(4)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(4)@jpeg", "did": "user(1)", "displayName": "bobby", "handle": "bob.test", @@ -102,8 +102,8 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(2)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(2)/cids(5)@jpeg", + "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", }, ], }, @@ -409,7 +409,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(4)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(4)@jpeg", "description": "hi im bob label_me", "did": "user(1)", "displayName": "bobby", @@ -445,7 +445,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -494,7 +494,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", diff --git a/packages/bsky/tests/_util.ts b/packages/bsky/tests/_util.ts index b984c5009f3..46a2eb5d901 100644 --- a/packages/bsky/tests/_util.ts +++ b/packages/bsky/tests/_util.ts @@ -52,17 +52,14 @@ export const forSnapshot = (obj: unknown) => { if (str.match(/^\d+::bafy/)) { return constantKeysetCursor } - if (str.match(/\/image\/[^/]+\/.+\/did:plc:[^/]+\/[^/]+@[\w]+$/)) { + if (str.match(/\/img\/[^/]+\/.+\/did:plc:[^/]+\/[^/]+@[\w]+$/)) { // Match image urls const match = str.match( - /\/image\/([^/]+)\/.+\/(did:plc:[^/]+)\/([^/]+)@[\w]+$/, + /\/img\/[^/]+\/.+\/(did:plc:[^/]+)\/([^/]+)@[\w]+$/, ) if (!match) return str - const [, sig, did, cid] = match - return str - .replace(sig, 'sig()') - .replace(did, take(users, did)) - .replace(cid, take(cids, cid)) + const [, did, cid] = match + return str.replace(did, take(users, did)).replace(cid, take(cids, cid)) } let isCid: boolean try { diff --git a/packages/bsky/tests/image/server.test.ts b/packages/bsky/tests/image/server.test.ts index b92f3c77e7c..072059b52df 100644 --- a/packages/bsky/tests/image/server.test.ts +++ b/packages/bsky/tests/image/server.test.ts @@ -6,6 +6,7 @@ import { TestNetwork } from '@atproto/dev-env' import { getInfo } from '../../src/image/sharp' import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' +import { ImageUriBuilder } from '../../src/image/uri' describe('image processing server', () => { let network: TestNetwork @@ -16,11 +17,6 @@ describe('image processing server', () => { beforeAll(async () => { network = await TestNetwork.create({ dbPostgresSchema: 'bsky_image_processing_server', - bsky: { - imgUriKey: - 'f23ecd142835025f42c3db2cf25dd813956c178392760256211f9d315f8ab4d8', - imgUriSalt: '9dd04221f5755bce5f55f47464c27e1e', - }, }) const pdsAgent = new AtpAgent({ service: network.pds.url }) const sc = new SeedClient(pdsAgent) @@ -30,7 +26,7 @@ describe('image processing server', () => { fileDid = sc.dids.carol fileCid = sc.posts[fileDid][0].images[0].image.ref client = axios.create({ - baseURL: `${network.bsky.url}/image`, + baseURL: `${network.bsky.url}/img`, validateStatus: () => true, }) }) @@ -41,42 +37,36 @@ describe('image processing server', () => { it('processes image from blob resolver.', async () => { const res = await client.get( - network.bsky.ctx.imgUriBuilder.getSignedPath({ + ImageUriBuilder.getPath({ + preset: 'feed_fullsize', did: fileDid, cid: fileCid, - format: 'jpeg', - fit: 'cover', - width: 500, - height: 500, - min: true, }), { responseType: 'stream' }, ) const info = await getInfo(res.data) - expect(info).toEqual( - expect.objectContaining({ - height: 500, - width: 500, - size: 67221, - }), - ) + + expect(info).toEqual({ + height: 580, + width: 1000, + size: 127578, + mime: 'image/jpeg', + }) expect(res.headers).toEqual( expect.objectContaining({ 'content-type': 'image/jpeg', 'cache-control': 'public, max-age=31536000', - 'content-length': '67221', + 'content-length': '127578', }), ) }) it('caches results.', async () => { - const path = network.bsky.ctx.imgUriBuilder.getSignedPath({ + const path = ImageUriBuilder.getPath({ + preset: 'avatar', did: fileDid, cid: fileCid, - format: 'jpeg', - width: 25, // Special number for this test - height: 25, }) const res1 = await client.get(path, { responseType: 'arraybuffer' }) expect(res1.headers['x-cache']).toEqual('miss') @@ -88,32 +78,13 @@ describe('image processing server', () => { expect(Buffer.compare(res1.data, res3.data)).toEqual(0) }) - it('errors on bad signature.', async () => { - const path = network.bsky.ctx.imgUriBuilder.getSignedPath({ - did: fileDid, - cid: fileCid, - format: 'jpeg', - fit: 'cover', - width: 500, - height: 500, - min: true, - }) - const res = await client.get(path.replace('/', '/_'), {}) - expect(res.status).toEqual(400) - expect(res.data).toEqual({ message: 'Invalid path: bad signature' }) - }) - it('errors on missing file.', async () => { const missingCid = await cidForCbor('missing-file') const res = await client.get( - network.bsky.ctx.imgUriBuilder.getSignedPath({ + ImageUriBuilder.getPath({ + preset: 'feed_fullsize', did: fileDid, cid: missingCid, - format: 'jpeg', - fit: 'cover', - width: 500, - height: 500, - min: true, }), ) expect(res.status).toEqual(404) diff --git a/packages/bsky/tests/image/uri.test.ts b/packages/bsky/tests/image/uri.test.ts index 63f0b3204f5..60586d23f6b 100644 --- a/packages/bsky/tests/image/uri.test.ts +++ b/packages/bsky/tests/image/uri.test.ts @@ -5,220 +5,80 @@ import { ImageUriBuilder, BadPathError } from '../../src/image/uri' describe('image uri builder', () => { let uriBuilder: ImageUriBuilder let cid: CID - const did = 'plc:did:xyz' + const did = 'did:plc:xyz' beforeAll(async () => { - const endpoint = 'https://example.com' - const salt = '9dd04221f5755bce5f55f47464c27e1e' - const key = - 'f23ecd142835025f42c3db2cf25dd813956c178392760256211f9d315f8ab4d8' - uriBuilder = new ImageUriBuilder(endpoint, salt, key) + const endpoint = 'https://example.com/img' + uriBuilder = new ImageUriBuilder(endpoint) cid = await cidForCbor('test cid') }) - it('signs and verifies uri options.', () => { - const path = uriBuilder.getSignedPath({ - did, - cid, - format: 'png', - height: 200, - width: 300, - }) - expect(path).toEqual( - `/1Hl07jYd8LUqPDAGVVw3Le2iT0OaH4l4dPbmh2lL21Y/rs:fill:300:200:0:0/plain/${did}/${cid.toString()}@png`, + it('generates paths.', () => { + expect(ImageUriBuilder.getPath({ preset: 'banner', did, cid })).toEqual( + `/banner/plain/${did}/${cid.toString()}@jpeg`, ) - expect(uriBuilder.getVerifiedOptions(path)).toEqual({ - signature: '1Hl07jYd8LUqPDAGVVw3Le2iT0OaH4l4dPbmh2lL21Y', - did, - cid, - format: 'png', - fit: 'cover', - height: 200, - width: 300, - min: false, - }) - }) - - it('errors on bad signature.', () => { - const tryGetVerifiedOptions = (path) => () => - uriBuilder.getVerifiedOptions(path) - - tryGetVerifiedOptions( - // Confirm this is a good signed uri - `/BtHM_4IOak5MOc2gOPDxbfS4_HG6VPcry2OAV03L29g/rs:fill:300:200:0:0/plain/${did}/${cid.toString()}@png`, - ) - - expect( - tryGetVerifiedOptions( - // Tamper with signature - `/DtHM_4IOak5MOc2gOPDxbfS4_HG6VPcry2OAV03L29g/rs:fill:300:200:0:0/plain/${did}/${cid.toString()}@png`, - ), - ).toThrow(new BadPathError('Invalid path: bad signature')) - expect( - tryGetVerifiedOptions( - // Tamper with params - `/DtHM_4IOak5MOc2gOPDxbfS4_HG6VPcry2OAV03L29g/rs:fill:300:200:0:0/plain/${did}/${cid.toString()}@jpeg`, - ), - ).toThrow(new BadPathError('Invalid path: bad signature')) - - expect( - tryGetVerifiedOptions( - // Missing signature - `/rs:fill:300:200:0:0/plain/${did}/${cid.toString()}@jpeg`, - ), - ).toThrow(new BadPathError('Invalid path: missing signature')) + ImageUriBuilder.getPath({ preset: 'feed_thumbnail', did, cid }), + ).toEqual(`/feed_thumbnail/plain/${did}/${cid.toString()}@jpeg`) }) - it('supports basic options.', () => { - const path = ImageUriBuilder.getPath({ - did, - cid, - format: 'png', - height: 200, - width: 300, - }) - expect(path).toEqual( - `/rs:fill:300:200:0:0/plain/${did}/${cid.toString()}@png`, + it('generates uris.', () => { + expect(uriBuilder.getPresetUri('banner', did, cid)).toEqual( + `https://example.com/img/banner/plain/${did}/${cid.toString()}@jpeg`, ) - expect(ImageUriBuilder.getOptions(path)).toEqual({ - did, - cid, - format: 'png', - fit: 'cover', - height: 200, - width: 300, - min: false, - }) - }) - - it('supports fit option.', () => { - const path = ImageUriBuilder.getPath({ - did, - cid, - format: 'png', - fit: 'inside', - height: 200, - width: 300, - }) - expect(path).toEqual( - `/rs:fit:300:200:0:0/plain/${did}/${cid.toString()}@png`, + expect( + uriBuilder.getPresetUri('feed_thumbnail', did, cid.toString()), + ).toEqual( + `https://example.com/img/feed_thumbnail/plain/${did}/${cid.toString()}@jpeg`, ) - expect(ImageUriBuilder.getOptions(path)).toEqual({ - did, - cid, - format: 'png', - fit: 'inside', - height: 200, - width: 300, - min: false, - }) }) - it('supports min=true option.', () => { - const path = ImageUriBuilder.getPath({ - did, - cid, - format: 'png', - height: 200, - width: 300, - min: true, - }) - expect(path).toEqual( - `/rs:fill:300:200:1:0/plain/${did}/${cid.toString()}@png`, - ) - expect(ImageUriBuilder.getOptions(path)).toEqual({ - did, + it('parses options.', () => { + expect( + ImageUriBuilder.getOptions(`/banner/plain/${did}/${cid.toString()}@png`), + ).toEqual({ + did: 'did:plc:xyz', cid, - format: 'png', fit: 'cover', - height: 200, - width: 300, + format: 'png', + height: 1000, min: true, + preset: 'banner', + width: 3000, }) - }) - - it('supports min={height,width} option.', () => { - const path = ImageUriBuilder.getPath({ - did, - cid, - format: 'jpeg', - height: 200, - width: 300, - min: { height: 50, width: 100 }, - }) - expect(path).toEqual( - `/rs:fill:300:200:0:0/mw:100/mh:50/plain/${did}/${cid.toString()}@jpeg`, - ) - expect(ImageUriBuilder.getOptions(path)).toEqual({ - did, + expect( + ImageUriBuilder.getOptions( + `/feed_thumbnail/plain/${did}/${cid.toString()}@jpeg`, + ), + ).toEqual({ + did: 'did:plc:xyz', cid, + fit: 'inside', format: 'jpeg', - fit: 'cover', - height: 200, - width: 300, - min: { height: 50, width: 100 }, + height: 2000, + min: true, + preset: 'feed_thumbnail', + width: 2000, }) }) - it('errors on bad did/cid/format part.', () => { - expect( - tryGetOptions(`/rs:fill:300:200:1:0/plain/${did}/${cid.toString()}@mp4`), - ).toThrow(new BadPathError('Invalid path: bad format')) - expect(tryGetOptions(`/rs:fill:300:200:1:0/plain/@jpg`)).toThrow( + it('errors on bad url pattern.', () => { + expect(tryGetOptions(`/a`)).toThrow(new BadPathError('Invalid path')) + expect(tryGetOptions(`/banner/plain/${did}@jpeg`)).toThrow( new BadPathError('Invalid path'), ) - expect( - tryGetOptions(`/rs:fill:300:200:1:0/plain/${did}/${cid.toString()}@`), - ).toThrow(new BadPathError('Invalid path')) - expect( - tryGetOptions(`/rs:fill:300:200:1:0/plain/${did}/${cid.toString()}@`), - ).toThrow(new BadPathError('Invalid path')) - expect( - tryGetOptions( - `/rs:fill:300:200:1:0/plain/${did}/${cid.toString()}@x@jpeg`, - ), - ).toThrow(new BadPathError('Invalid path: bad format')) }) - it('errors on mismatching min settings.', () => { - expect( - tryGetOptions( - `/rs:fill:300:200:1:0/mw:100/mh:50/plain/${did}/${did}/${cid.toString()}@jpeg`, - ), - ).toThrow(new BadPathError('Invalid path: bad min width/height param')) + it('errors on bad preset.', () => { expect( - tryGetOptions( - `/rs:fill:300:200:0:0/mw:100/plain/${did}/${did}/${cid.toString()}@jpeg`, - ), - ).toThrow(new BadPathError('Invalid path: bad min width/height param')) + tryGetOptions(`/bad_banner/plain/${did}/${cid.toString()}@jpeg`), + ).toThrow(new BadPathError('Invalid path: bad preset')) }) - it('errors on bad fit setting.', () => { + it('errors on bad format.', () => { expect( - tryGetOptions( - `/rs:blah:300:200:1:0/plain/${did}/${did}/${cid.toString()}@jpeg`, - ), - ).toThrow(new BadPathError('Invalid path: bad resize fit param')) - }) - - it('errors on bad dimension settings.', () => { - expect( - tryGetOptions(`/rs:fill:30x:200:1:0/plain/${did}/${cid.toString()}@jpeg`), - ).toThrow(new BadPathError('Invalid path: bad resize height/width param')) - expect( - tryGetOptions(`/rs:fill:300:20x:1:0/plain/${did}/${cid.toString()}@jpeg`), - ).toThrow(new BadPathError('Invalid path: bad resize height/width param')) - expect( - tryGetOptions( - `/rs:fill:300:200:1:0/mw:10x/mh:50/plain/${did}/${cid.toString()}@jpeg`, - ), - ).toThrow(new BadPathError('Invalid path: bad min width/height param')) - expect( - tryGetOptions( - `/rs:fill:300:200:1:0/mw:100/mh:5x/plain/${did}/${cid.toString()}@jpeg`, - ), - ).toThrow(new BadPathError('Invalid path: bad min width/height param')) + tryGetOptions(`/banner/plain/${did}/${cid.toString()}@webp`), + ).toThrow(new BadPathError('Invalid path: bad format')) }) function tryGetOptions(path: string) { diff --git a/packages/bsky/tests/moderation.test.ts b/packages/bsky/tests/moderation.test.ts index 946b2d29a30..7c7e50797ca 100644 --- a/packages/bsky/tests/moderation.test.ts +++ b/packages/bsky/tests/moderation.test.ts @@ -1067,7 +1067,7 @@ describe('moderation', () => { post = sc.posts[sc.dids.carol][0] blob = post.images[1] imageUri = ctx.imgUriBuilder - .getCommonSignedUri( + .getPresetUri( 'feed_thumbnail', sc.dids.carol, blob.image.ref.toString(), diff --git a/packages/bsky/tests/views/__snapshots__/actor-search.test.ts.snap b/packages/bsky/tests/views/__snapshots__/actor-search.test.ts.snap index c0441c49b6d..a6990b60ed3 100644 --- a/packages/bsky/tests/views/__snapshots__/actor-search.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/actor-search.test.ts.snap @@ -3,7 +3,7 @@ exports[`pds actor search views search gives relevant results 1`] = ` Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "did": "user(0)", "displayName": "Carlton Abernathy IV", "handle": "aliya-hodkiewicz.test", @@ -33,7 +33,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "did": "user(4)", "displayName": "Latoya Windler", "handle": "carolina-mcdermott77.test", @@ -45,7 +45,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(7)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(0)@jpeg", "did": "user(6)", "displayName": "Rachel Kshlerin", "handle": "cayla-marquardt39.test", @@ -57,7 +57,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(9)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(9)/cids(0)@jpeg", "did": "user(8)", "displayName": "Carol Littel", "handle": "eudora-dietrich4.test", @@ -69,7 +69,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(11)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(11)/cids(0)@jpeg", "did": "user(10)", "displayName": "Sadie Carter", "handle": "shane-torphy52.test", @@ -86,7 +86,7 @@ Array [ exports[`pds actor search views typeahead gives relevant results 1`] = ` Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "did": "user(0)", "displayName": "Carlton Abernathy IV", "handle": "aliya-hodkiewicz.test", @@ -112,7 +112,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "did": "user(4)", "displayName": "Latoya Windler", "handle": "carolina-mcdermott77.test", @@ -122,7 +122,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(7)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(0)@jpeg", "did": "user(6)", "displayName": "Rachel Kshlerin", "handle": "cayla-marquardt39.test", @@ -132,7 +132,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(9)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(9)/cids(0)@jpeg", "did": "user(8)", "displayName": "Carol Littel", "handle": "eudora-dietrich4.test", @@ -142,7 +142,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(11)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(11)/cids(0)@jpeg", "did": "user(10)", "displayName": "Sadie Carter", "handle": "shane-torphy52.test", 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 7eaebb72be6..e66085e7096 100644 --- a/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap @@ -5,7 +5,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -43,7 +43,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -61,8 +61,8 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(3)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(3)/cids(4)@jpeg", + "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", }, ], }, @@ -125,7 +125,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -154,7 +154,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -308,7 +308,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -336,7 +336,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -369,7 +369,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "bobby", "handle": "bob.test", @@ -385,8 +385,8 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(1)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(1)/cids(2)@jpeg", + "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", }, ], }, @@ -450,7 +450,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "ali", "handle": "alice.test", @@ -481,7 +481,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "ali", "handle": "alice.test", @@ -514,7 +514,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "bobby", "handle": "bob.test", @@ -542,7 +542,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "bobby", "handle": "bob.test", @@ -610,13 +610,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(3)@jpeg", + "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", }, ], }, @@ -624,7 +624,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -787,7 +787,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -818,7 +818,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -867,13 +867,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(3)@jpeg", + "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", }, ], }, @@ -881,7 +881,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -967,7 +967,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1043,13 +1043,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(4)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(4)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "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", }, ], }, @@ -1057,7 +1057,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -1200,7 +1200,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1240,7 +1240,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -1257,8 +1257,8 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(3)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(3)/cids(4)@jpeg", + "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", }, ], }, @@ -1321,7 +1321,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1354,7 +1354,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1509,7 +1509,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1541,7 +1541,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", diff --git a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap index 582de290e96..57688462432 100644 --- a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap @@ -6,7 +6,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -116,7 +116,7 @@ Object { }, "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -163,7 +163,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -201,7 +201,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -218,8 +218,8 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(3)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(3)/cids(3)@jpeg", + "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", }, ], }, diff --git a/packages/bsky/tests/views/__snapshots__/follows.test.ts.snap b/packages/bsky/tests/views/__snapshots__/follows.test.ts.snap index 2f21896ef62..2061726d8e6 100644 --- a/packages/bsky/tests/views/__snapshots__/follows.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/follows.test.ts.snap @@ -5,7 +5,7 @@ Object { "cursor": "0000000000000::bafycid", "followers": Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-eve", "did": "user(2)", "displayName": "display-eve", @@ -20,7 +20,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "descript-dan", "did": "user(4)", "displayName": "display-dan", @@ -35,7 +35,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(7)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(0)@jpeg", "description": "descript-bob", "did": "user(6)", "displayName": "display-bob", @@ -50,7 +50,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(9)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(9)/cids(0)@jpeg", "description": "descript-carol", "did": "user(8)", "displayName": "display-carol", @@ -66,7 +66,7 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-alice", "did": "user(0)", "displayName": "display-alice", @@ -86,7 +86,7 @@ Object { "cursor": "0000000000000::bafycid", "followers": Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-dan", "did": "user(2)", "displayName": "display-dan", @@ -101,7 +101,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "descript-alice", "did": "user(4)", "displayName": "display-alice", @@ -115,7 +115,7 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-bob", "did": "user(0)", "displayName": "display-bob", @@ -137,7 +137,7 @@ Object { "cursor": "0000000000000::bafycid", "followers": Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-eve", "did": "user(2)", "displayName": "display-eve", @@ -152,7 +152,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "descript-bob", "did": "user(4)", "displayName": "display-bob", @@ -167,7 +167,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(7)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(0)@jpeg", "description": "descript-alice", "did": "user(6)", "displayName": "display-alice", @@ -181,7 +181,7 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-carol", "did": "user(0)", "displayName": "display-carol", @@ -203,7 +203,7 @@ Object { "cursor": "0000000000000::bafycid", "followers": Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-alice", "did": "user(2)", "displayName": "display-alice", @@ -217,7 +217,7 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-dan", "did": "user(0)", "displayName": "display-dan", @@ -239,7 +239,7 @@ Object { "cursor": "0000000000000::bafycid", "followers": Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-dan", "did": "user(2)", "displayName": "display-dan", @@ -254,7 +254,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "descript-alice", "did": "user(4)", "displayName": "display-alice", @@ -268,7 +268,7 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-eve", "did": "user(0)", "displayName": "display-eve", @@ -290,7 +290,7 @@ Object { "cursor": "0000000000000::bafycid", "follows": Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-eve", "did": "user(2)", "displayName": "display-eve", @@ -305,7 +305,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "descript-dan", "did": "user(4)", "displayName": "display-dan", @@ -320,7 +320,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(7)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(0)@jpeg", "description": "descript-carol", "did": "user(6)", "displayName": "display-carol", @@ -335,7 +335,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(9)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(9)/cids(0)@jpeg", "description": "descript-bob", "did": "user(8)", "displayName": "display-bob", @@ -351,7 +351,7 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-alice", "did": "user(0)", "displayName": "display-alice", @@ -371,7 +371,7 @@ Object { "cursor": "0000000000000::bafycid", "follows": Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-carol", "did": "user(2)", "displayName": "display-carol", @@ -386,7 +386,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "descript-alice", "did": "user(4)", "displayName": "display-alice", @@ -400,7 +400,7 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-bob", "did": "user(0)", "displayName": "display-bob", @@ -422,7 +422,7 @@ Object { "cursor": "0000000000000::bafycid", "follows": Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-alice", "did": "user(2)", "displayName": "display-alice", @@ -436,7 +436,7 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-carol", "did": "user(0)", "displayName": "display-carol", @@ -458,7 +458,7 @@ Object { "cursor": "0000000000000::bafycid", "follows": Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-eve", "did": "user(2)", "displayName": "display-eve", @@ -473,7 +473,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "descript-bob", "did": "user(4)", "displayName": "display-bob", @@ -488,7 +488,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(7)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(0)@jpeg", "description": "descript-alice", "did": "user(6)", "displayName": "display-alice", @@ -502,7 +502,7 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-dan", "did": "user(0)", "displayName": "display-dan", @@ -524,7 +524,7 @@ Object { "cursor": "0000000000000::bafycid", "follows": Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-carol", "did": "user(2)", "displayName": "display-carol", @@ -539,7 +539,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "descript-alice", "did": "user(4)", "displayName": "display-alice", @@ -553,7 +553,7 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-eve", "did": "user(0)", "displayName": "display-eve", diff --git a/packages/bsky/tests/views/__snapshots__/likes.test.ts.snap b/packages/bsky/tests/views/__snapshots__/likes.test.ts.snap index ec116eb4b49..53f37486d1c 100644 --- a/packages/bsky/tests/views/__snapshots__/likes.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/likes.test.ts.snap @@ -48,7 +48,7 @@ Object { }, Object { "actor": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(0)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", 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 2b8a2a55aa4..97cc1b2ad6e 100644 --- a/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap @@ -3,7 +3,7 @@ exports[`bsky views with mutes from mute lists embeds lists in posts 1`] = ` Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -20,7 +20,7 @@ Object { "$type": "app.bsky.graph.defs#listView", "cid": "cids(3)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -66,7 +66,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -108,7 +108,7 @@ Object { "following": "record(6)", "muted": true, "mutedByList": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "name": "alice mutes", @@ -149,7 +149,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -159,7 +159,7 @@ Object { "following": "record(9)", "muted": true, "mutedByList": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "name": "alice mutes", @@ -177,8 +177,8 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(4)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(4)/cids(5)@jpeg", + "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", }, ], }, @@ -250,7 +250,7 @@ Object { Object { "cid": "cids(0)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "description": "its me!", "did": "user(0)", "displayName": "ali", @@ -273,10 +273,10 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "cid": "cids(2)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "description": "its me!", "did": "user(0)", "displayName": "ali", @@ -309,7 +309,7 @@ Object { Object { "cid": "cids(0)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "description": "its me!", "did": "user(0)", "displayName": "ali", @@ -332,10 +332,10 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "cid": "cids(2)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "description": "its me!", "did": "user(0)", "displayName": "ali", @@ -375,7 +375,7 @@ Object { "followedBy": "record(1)", "muted": true, "mutedByList": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "name": "alice mutes", @@ -390,7 +390,7 @@ Object { }, Object { "subject": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "description": "hi im bob label_me", "did": "user(2)", "displayName": "bobby", @@ -402,7 +402,7 @@ Object { "following": "record(2)", "muted": true, "mutedByList": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "name": "alice mutes", @@ -417,10 +417,10 @@ Object { }, ], "list": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "cid": "cids(0)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "description": "its me!", "did": "user(4)", "displayName": "ali", diff --git a/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap b/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap index 0e91aad70c9..181022cc578 100644 --- a/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap @@ -3,7 +3,7 @@ exports[`mute views fetches mutes for the logged-in user. 1`] = ` Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "did": "user(0)", "displayName": "Dr. Lowell DuBuque", "handle": "elta48.test", @@ -15,7 +15,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "did": "user(2)", "displayName": "Sally Funk", "handle": "magnus53.test", @@ -36,7 +36,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(0)@jpeg", "did": "user(5)", "displayName": "Patrick Sawayn", "handle": "jeffrey-sawayn87.test", @@ -48,7 +48,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(8)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(8)/cids(0)@jpeg", "did": "user(7)", "displayName": "Kim Streich", "handle": "adrienne49.test", @@ -60,7 +60,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(10)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(10)/cids(0)@jpeg", "did": "user(9)", "displayName": "Carlton Abernathy IV", "handle": "aliya-hodkiewicz.test", @@ -82,7 +82,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(13)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(13)/cids(0)@jpeg", "description": "hi im bob label_me", "did": "user(12)", "displayName": "bobby", @@ -103,7 +103,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -171,7 +171,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -189,8 +189,8 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(4)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(4)/cids(4)@jpeg", + "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", }, ], }, diff --git a/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap b/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap index c516d6803ce..8b4bc79de25 100644 --- a/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap @@ -173,7 +173,7 @@ Array [ }, Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(10)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(10)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -201,7 +201,7 @@ Array [ }, Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(10)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(10)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -273,7 +273,7 @@ Array [ }, Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(10)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(10)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -305,7 +305,7 @@ Array [ }, Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(10)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(10)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -590,7 +590,7 @@ Array [ }, Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(13)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(13)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -618,7 +618,7 @@ Array [ }, Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(13)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(13)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -690,7 +690,7 @@ Array [ }, Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(13)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(13)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -722,7 +722,7 @@ Array [ }, Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(13)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(13)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -785,7 +785,7 @@ Array [ }, Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(3)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(3)@jpeg", "description": "its me!", "did": "user(1)", "displayName": "ali", @@ -812,7 +812,7 @@ Array [ }, Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(3)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(3)@jpeg", "description": "its me!", "did": "user(1)", "displayName": "ali", diff --git a/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap b/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap index 3df6a281cfe..75392f67e85 100644 --- a/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap @@ -4,7 +4,7 @@ exports[`pds posts views fetches posts 1`] = ` Array [ Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -30,7 +30,7 @@ Array [ }, Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -56,7 +56,7 @@ Array [ }, Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -106,13 +106,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(5)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(5)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(6)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(6)@jpeg", + "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", }, ], }, @@ -120,7 +120,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -236,13 +236,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(5)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(5)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(6)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(6)@jpeg", + "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", }, ], }, @@ -250,7 +250,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -361,7 +361,7 @@ Array [ }, Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", diff --git a/packages/bsky/tests/views/__snapshots__/profile.test.ts.snap b/packages/bsky/tests/views/__snapshots__/profile.test.ts.snap index bf1072ed2e8..8308d94bc6f 100644 --- a/packages/bsky/tests/views/__snapshots__/profile.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/profile.test.ts.snap @@ -3,7 +3,7 @@ exports[`pds profile views fetches multiple profiles 1`] = ` Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "its me!", "did": "user(0)", "displayName": "ali", @@ -21,7 +21,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "hi im bob label_me", "did": "user(2)", "displayName": "bobby", @@ -67,7 +67,7 @@ Array [ exports[`pds profile views fetches other's profile, with a follow 1`] = ` Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "its me!", "did": "user(0)", "displayName": "ali", @@ -104,7 +104,7 @@ Object { exports[`pds profile views fetches own profile 1`] = ` Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "its me!", "did": "user(0)", "displayName": "ali", @@ -123,8 +123,8 @@ Object { exports[`pds profile views presents avatars & banners 1`] = ` Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", - "banner": "https://bsky.public.url/image/sig()/rs:fill:3000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", + "banner": "https://bsky.public.url/img/banner/plain/user(1)/cids(1)@jpeg", "description": "new descript", "did": "user(0)", "displayName": "ali", diff --git a/packages/bsky/tests/views/__snapshots__/reposts.test.ts.snap b/packages/bsky/tests/views/__snapshots__/reposts.test.ts.snap index cf5f9910211..e927d73dfac 100644 --- a/packages/bsky/tests/views/__snapshots__/reposts.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/reposts.test.ts.snap @@ -33,7 +33,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(0)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", diff --git a/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap b/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap index 846f19ec0b7..7a56218258e 100644 --- a/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap @@ -9,7 +9,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -41,7 +41,7 @@ Object { }, "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -57,8 +57,8 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(3)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(3)/cids(4)@jpeg", + "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", }, ], }, @@ -122,7 +122,7 @@ Object { }, "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -169,7 +169,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -241,7 +241,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -257,8 +257,8 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(4)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(4)/cids(4)@jpeg", + "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", }, ], }, @@ -323,7 +323,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -374,7 +374,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -445,7 +445,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -461,8 +461,8 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(4)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(4)/cids(4)@jpeg", + "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", }, ], }, @@ -532,7 +532,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -563,7 +563,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -602,7 +602,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -651,7 +651,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -691,7 +691,7 @@ Object { }, "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -741,7 +741,7 @@ Object { }, "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -793,7 +793,7 @@ Object { }, "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -840,7 +840,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -873,7 +873,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -889,8 +889,8 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(3)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(3)/cids(3)@jpeg", + "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", }, ], }, @@ -955,7 +955,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1006,7 +1006,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1039,7 +1039,7 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -1055,8 +1055,8 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(3)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(3)/cids(3)@jpeg", + "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", }, ], }, diff --git a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap index 55fd71ed34c..17af93d6cea 100644 --- a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap @@ -5,7 +5,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -47,7 +47,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -85,7 +85,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -184,7 +184,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -294,7 +294,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -327,7 +327,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -369,7 +369,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -445,7 +445,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -472,7 +472,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -501,7 +501,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -552,7 +552,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -582,7 +582,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -655,13 +655,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(10)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(10)@jpeg", + "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", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(11)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(11)@jpeg", + "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", }, ], }, @@ -669,7 +669,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -771,7 +771,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -814,7 +814,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -847,7 +847,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -923,13 +923,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(4)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(4)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "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", }, ], }, @@ -937,7 +937,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -1083,7 +1083,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1121,7 +1121,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -1139,8 +1139,8 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(4)@jpeg", + "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", }, ], }, @@ -1203,7 +1203,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1270,7 +1270,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1297,7 +1297,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1326,7 +1326,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -1344,8 +1344,8 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(4)@jpeg", + "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", }, ], }, @@ -1409,7 +1409,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1436,7 +1436,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1465,7 +1465,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1628,7 +1628,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -1658,7 +1658,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1720,13 +1720,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(4)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(4)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "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", }, ], }, @@ -1734,7 +1734,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -1910,13 +1910,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(4)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(4)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "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", }, ], }, @@ -1924,7 +1924,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -2026,7 +2026,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -2069,7 +2069,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -2135,13 +2135,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(3)@jpeg", + "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", }, ], }, @@ -2149,7 +2149,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -2292,7 +2292,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -2332,7 +2332,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -2348,8 +2348,8 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(4)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(4)/cids(2)@jpeg", + "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", }, ], }, @@ -2412,7 +2412,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -2482,7 +2482,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -2513,7 +2513,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -2546,7 +2546,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -2562,8 +2562,8 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(4)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(4)/cids(2)@jpeg", + "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", }, ], }, @@ -2627,7 +2627,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -2658,7 +2658,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -2691,7 +2691,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -2857,7 +2857,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -2885,7 +2885,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -2934,13 +2934,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(3)@jpeg", + "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", }, ], }, @@ -2948,7 +2948,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -3048,7 +3048,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -3089,7 +3089,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3155,13 +3155,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(3)@jpeg", + "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", }, ], }, @@ -3169,7 +3169,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -3314,7 +3314,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3354,7 +3354,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -3371,8 +3371,8 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(4)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(4)/cids(2)@jpeg", + "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", }, ], }, @@ -3435,7 +3435,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3504,7 +3504,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3535,7 +3535,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3568,7 +3568,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3732,7 +3732,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3780,13 +3780,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(3)@jpeg", + "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", }, ], }, @@ -3794,7 +3794,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -3893,7 +3893,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(6)/cids(5)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -3928,7 +3928,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -3973,7 +3973,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -3990,8 +3990,8 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(4)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(4)/cids(3)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(3)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(3)@jpeg", }, ], }, @@ -4055,7 +4055,7 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -4086,7 +4086,7 @@ Array [ "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -4119,7 +4119,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -4179,13 +4179,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(3)@jpeg", + "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", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(7)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(7)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(7)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(7)@jpeg", }, ], }, @@ -4193,7 +4193,7 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", @@ -4349,7 +4349,7 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", "did": "user(3)", "displayName": "bobby", "handle": "bob.test", diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index 01f085789ae..abb9725f6a6 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -39,9 +39,6 @@ export class TestBsky { didPlcUrl: cfg.plcUrl, publicUrl: 'https://bsky.public.url', serverDid, - imgUriSalt: '9dd04221f5755bce5f55f47464c27e1e', - imgUriKey: - 'f23ecd142835025f42c3db2cf25dd813956c178392760256211f9d315f8ab4d8', didCacheStaleTTL: HOUR, didCacheMaxTTL: DAY, ...cfg, diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index 33b14030074..5d0ed301d52 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -244,7 +244,7 @@ export const forSnapshot = (obj: unknown) => { return constantDidCursor } if (str.match(/\/image\/[^/]+\/.+\/did:plc:[^/]+\/[^/]+@[\w]+$/)) { - // Match image urls + // Match image urls (pds) const match = str.match( /\/image\/([^/]+)\/.+\/(did:plc:[^/]+)\/([^/]+)@[\w]+$/, ) @@ -255,6 +255,15 @@ export const forSnapshot = (obj: unknown) => { .replace(did, take(users, did)) .replace(cid, take(cids, cid)) } + if (str.match(/\/img\/[^/]+\/.+\/did:plc:[^/]+\/[^/]+@[\w]+$/)) { + // Match image urls (bsky w/ presets) + 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)) + } if (str.startsWith('pds-public-url-')) { return 'invite-code' } diff --git a/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap index a2c8de36c91..dddb9f6ba6c 100644 --- a/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap @@ -7,7 +7,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -45,7 +45,7 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -63,8 +63,8 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(3)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(3)/cids(4)@jpeg", + "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", }, ], }, @@ -127,7 +127,7 @@ Object { "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -194,7 +194,7 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -221,7 +221,7 @@ Object { "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -250,7 +250,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -268,8 +268,8 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(3)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(3)/cids(4)@jpeg", + "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", }, ], }, @@ -333,7 +333,7 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -360,7 +360,7 @@ Object { "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -389,7 +389,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -543,7 +543,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -573,7 +573,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -619,13 +619,13 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(4)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(4)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(9)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(9)@jpeg", + "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", }, ], }, @@ -633,7 +633,7 @@ Object { "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -717,7 +717,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -751,7 +751,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", diff --git a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap index 64792c40b71..85269227bbb 100644 --- a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap @@ -2,7 +2,7 @@ exports[`proxies view requests actor.getProfile 1`] = ` Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "hi im bob label_me", "did": "user(0)", "displayName": "bobby", @@ -25,7 +25,7 @@ exports[`proxies view requests actor.getProfiles 1`] = ` Object { "profiles": Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "its me!", "did": "user(0)", "displayName": "ali", @@ -41,7 +41,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "hi im bob label_me", "did": "user(2)", "displayName": "bobby", @@ -66,7 +66,7 @@ exports[`proxies view requests actor.getSuggestions 1`] = ` Object { "actors": Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(0)@jpeg", "description": "hi im bob label_me", "did": "user(1)", "displayName": "bobby", @@ -96,7 +96,7 @@ Object { exports[`proxies view requests actor.searchActor 1`] = ` Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "its me!", "did": "user(0)", "displayName": "ali", @@ -109,7 +109,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "hi im bob label_me", "did": "user(2)", "displayName": "bobby", @@ -150,7 +150,7 @@ Array [ exports[`proxies view requests actor.searchActorTypeahead 1`] = ` Array [ Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -160,7 +160,7 @@ Array [ }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "did": "user(2)", "displayName": "bobby", "handle": "bob.test", @@ -200,7 +200,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "bobby", "handle": "bob.test", @@ -218,8 +218,8 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(1)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(1)/cids(2)@jpeg", + "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", }, ], }, @@ -283,7 +283,7 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "ali", "handle": "alice.test", @@ -310,7 +310,7 @@ Object { "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", "did": "user(2)", "displayName": "ali", "handle": "alice.test", @@ -339,7 +339,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "bobby", "handle": "bob.test", @@ -369,7 +369,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "bobby", "handle": "bob.test", @@ -411,7 +411,7 @@ Object { "view": Object { "cid": "cids(0)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -437,7 +437,7 @@ Object { Object { "cid": "cids(0)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", @@ -464,7 +464,7 @@ Object { "likes": Array [ Object { "actor": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "hi im bob label_me", "did": "user(0)", "displayName": "bobby", @@ -483,7 +483,7 @@ Object { }, Object { "actor": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "its me!", "did": "user(2)", "displayName": "ali", @@ -508,7 +508,7 @@ Object { "posts": Array [ Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "bobby", "handle": "bob.test", @@ -558,13 +558,13 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(3)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(3)/cids(3)@jpeg", + "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/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(3)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(3)/cids(4)@jpeg", + "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", }, ], }, @@ -572,7 +572,7 @@ Object { "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", "displayName": "bobby", "handle": "bob.test", @@ -683,7 +683,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1).jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -759,13 +759,13 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(4).jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(4).jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "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", }, ], }, @@ -773,7 +773,7 @@ Object { "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1).jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -901,7 +901,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1).jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -939,7 +939,7 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1).jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -957,8 +957,8 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(4)@jpeg", + "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", }, ], }, @@ -1021,7 +1021,7 @@ Object { "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1).jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1088,7 +1088,7 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1).jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1115,7 +1115,7 @@ Object { "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1).jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1144,7 +1144,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1).jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -1162,8 +1162,8 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(5)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(5)/cids(4)@jpeg", + "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", }, ], }, @@ -1227,7 +1227,7 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1).jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1254,7 +1254,7 @@ Object { "root": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1).jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1283,7 +1283,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1).jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1437,7 +1437,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1).jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -1467,7 +1467,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1).jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1529,13 +1529,13 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(4).jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(4).jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "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", }, ], }, @@ -1543,7 +1543,7 @@ Object { "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1).jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -1701,13 +1701,13 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(4).jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(4).jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/image/sig()/rs:fit:2000:2000:1:0/plain/user(6)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/image/sig()/rs:fit:1000:1000:1:0/plain/user(6)/cids(5)@jpeg", + "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", }, ], }, @@ -1715,7 +1715,7 @@ Object { "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1).jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -1799,7 +1799,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(5)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1).jpeg", "did": "user(4)", "displayName": "bobby", "handle": "bob.test", @@ -1833,7 +1833,7 @@ Object { Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1).jpeg", "did": "user(0)", "displayName": "ali", "handle": "alice.test", @@ -1878,7 +1878,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(0)@jpeg", "description": "hi im bob label_me", "did": "user(1)", "displayName": "bobby", @@ -1913,7 +1913,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(0)@jpeg", "description": "its me!", "did": "user(3)", "displayName": "ali", @@ -1927,7 +1927,7 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "hi im bob label_me", "did": "user(0)", "displayName": "bobby", @@ -1960,7 +1960,7 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(0)@jpeg", "description": "its me!", "did": "user(3)", "displayName": "ali", @@ -1974,7 +1974,7 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "hi im bob label_me", "did": "user(0)", "displayName": "bobby", @@ -2010,7 +2010,7 @@ Object { }, Object { "subject": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(2)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(0)@jpeg", "description": "its me!", "did": "user(1)", "displayName": "ali", @@ -2027,7 +2027,7 @@ Object { "list": Object { "cid": "cids(1)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(4)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(0)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -2060,7 +2060,7 @@ Object { Object { "cid": "cids(0)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "description": "hi im bob label_me", "did": "user(0)", "displayName": "bobby", @@ -2086,7 +2086,7 @@ Object { Object { "cid": "cids(2)", "creator": Object { - "avatar": "https://bsky.public.url/image/sig()/rs:fill:1000:1000:1:0/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "description": "hi im bob label_me", "did": "user(0)", "displayName": "bobby", From 0bf75fef6034c97d442e23bf4a67055b36655ff2 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 9 Aug 2023 13:42:03 -0500 Subject: [PATCH 120/237] Add bin for launching dev-env with an appview (#1454) script for launching a full dev-env network --- packages/dev-env/build.js | 2 +- packages/dev-env/package.json | 1 + packages/dev-env/src/bin-network.ts | 40 +++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 packages/dev-env/src/bin-network.ts diff --git a/packages/dev-env/build.js b/packages/dev-env/build.js index fd0a02aa468..a6cff485859 100644 --- a/packages/dev-env/build.js +++ b/packages/dev-env/build.js @@ -14,7 +14,7 @@ if (process.argv.includes('--update-main-to-dist')) { require('esbuild').build({ logLevel: 'info', - entryPoints: ['src/index.ts', 'src/bin.ts'], + entryPoints: ['src/index.ts', 'src/bin.ts', 'src/bin-network.ts'], bundle: true, sourcemap: true, outdir: 'dist', diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index e12e73d02b0..830cf630bf7 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -13,6 +13,7 @@ "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "start": "node dist/bin.js", + "start:network": "../dev-infra/with-test-redis-and-db.sh node dist/bin-network.js", "prettier": "prettier --check src/", "prettier:fix": "prettier --write src/", "lint": "eslint . --ext .ts,.tsx", diff --git a/packages/dev-env/src/bin-network.ts b/packages/dev-env/src/bin-network.ts new file mode 100644 index 00000000000..56f0138b3b2 --- /dev/null +++ b/packages/dev-env/src/bin-network.ts @@ -0,0 +1,40 @@ +import { generateMockSetup } from './mock' +import { TestNetwork } from './network' + +const run = async () => { + console.log(` +██████╗ +██╔═══██╗ +██║██╗██║ +██║██║██║ +╚█║████╔╝ + ╚╝╚═══╝ protocol + +[ created by Bluesky ]`) + + const network = await TestNetwork.create({ + pds: { + port: 2583, + publicUrl: 'http://localhost:2583', + dbPostgresSchema: 'pds', + }, + bsky: { + dbPostgresSchema: 'bsky', + }, + plc: { port: 2582 }, + }) + await generateMockSetup(network) + + console.log( + `👤 DID Placeholder server started http://localhost:${network.plc.port}`, + ) + console.log( + `🌞 Personal Data server started http://localhost:${network.pds.port}`, + ) + console.log(`🌅 Bsky Appview started http://localhost:${network.bsky.port}`) + for (const fg of network.feedGens) { + console.log(`🤖 Feed Generator started http://localhost:${fg.port}`) + } +} + +run() From f41e76a653ea90a9ab9376624ab6018868e2c1c7 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 9 Aug 2023 14:04:23 -0500 Subject: [PATCH 121/237] tighten up tests --- packages/bsky/tests/views/author-feed.test.ts | 17 ++++++++++++-- packages/pds/tests/views/author-feed.test.ts | 22 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index b8041573cde..f27e778fddb 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -249,7 +249,9 @@ describe('pds author feed views', () => { expect( allFeed.feed.some(({ post }) => { return ( - isEmbedRecordWithMedia(post.embed) && isImageEmbed(post.embed?.media) + (isEmbedRecordWithMedia(post.embed) && + isImageEmbed(post.embed?.media)) || + isImageEmbed(post.embed) ) }), ).toBeTruthy() @@ -258,14 +260,25 @@ describe('pds author feed views', () => { actor: carol, filter: 'posts_with_media', }) + const { data: imagesOnlyFeed } = await agent.api.app.bsky.feed.getAuthorFeed({ + actor: bob, + filter: 'posts_with_media', + }) expect( mediaFeed.feed.every(({ post }) => { return ( - isEmbedRecordWithMedia(post.embed) && isImageEmbed(post.embed?.media) + (isEmbedRecordWithMedia(post.embed) && + isImageEmbed(post.embed?.media)) || + isImageEmbed(post.embed) ) }), ).toBeTruthy() + expect( + imagesOnlyFeed.feed.every(({ post }) => { + return isImageEmbed(post.embed) + }), + ).toBeTruthy() const { data: postsOnlyFeed } = await agent.api.app.bsky.feed.getAuthorFeed( { actor: carol, filter: 'posts_no_replies' }, diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index 307570ff25b..b9d32fd1400 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -293,7 +293,9 @@ describe('pds author feed views', () => { expect( allFeed.feed.some(({ post }) => { return ( - isEmbedRecordWithMedia(post.embed) && isImageEmbed(post.embed?.media) + (isEmbedRecordWithMedia(post.embed) && + isImageEmbed(post.embed?.media)) || + isImageEmbed(post.embed) ) }), ).toBeTruthy() @@ -307,14 +309,30 @@ describe('pds author feed views', () => { headers: sc.getHeaders(alice), }, ) + const { data: imagesOnlyFeed } = await agent.api.app.bsky.feed.getAuthorFeed( + { + actor: bob, + filter: 'posts_with_media', + }, + { + headers: sc.getHeaders(alice), + }, + ) expect( mediaFeed.feed.every(({ post }) => { return ( - isEmbedRecordWithMedia(post.embed) && isImageEmbed(post.embed?.media) + (isEmbedRecordWithMedia(post.embed) && + isImageEmbed(post.embed?.media)) || + isImageEmbed(post.embed) ) }), ).toBeTruthy() + expect( + imagesOnlyFeed.feed.every(({ post }) => { + return isImageEmbed(post.embed) + }), + ).toBeTruthy() const { data: postsOnlyFeed } = await agent.api.app.bsky.feed.getAuthorFeed( { actor: carol, filter: 'posts_no_replies' }, From 20230b46bcdc1d983b191d542765f9ea9c9a28c8 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 9 Aug 2023 14:05:03 -0500 Subject: [PATCH 122/237] format --- packages/bsky/tests/views/author-feed.test.ts | 9 +++++---- packages/pds/tests/views/author-feed.test.ts | 19 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index f27e778fddb..515a826dcde 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -260,10 +260,11 @@ describe('pds author feed views', () => { actor: carol, filter: 'posts_with_media', }) - const { data: imagesOnlyFeed } = await agent.api.app.bsky.feed.getAuthorFeed({ - actor: bob, - filter: 'posts_with_media', - }) + const { data: imagesOnlyFeed } = + await agent.api.app.bsky.feed.getAuthorFeed({ + actor: bob, + filter: 'posts_with_media', + }) expect( mediaFeed.feed.every(({ post }) => { diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index b9d32fd1400..389d1f752d3 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -309,15 +309,16 @@ describe('pds author feed views', () => { headers: sc.getHeaders(alice), }, ) - const { data: imagesOnlyFeed } = await agent.api.app.bsky.feed.getAuthorFeed( - { - actor: bob, - filter: 'posts_with_media', - }, - { - headers: sc.getHeaders(alice), - }, - ) + const { data: imagesOnlyFeed } = + await agent.api.app.bsky.feed.getAuthorFeed( + { + actor: bob, + filter: 'posts_with_media', + }, + { + headers: sc.getHeaders(alice), + }, + ) expect( mediaFeed.feed.every(({ post }) => { From ab50816461116214daa9e2b8e461a8bba4844ed8 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 9 Aug 2023 16:06:29 -0700 Subject: [PATCH 123/237] Self-labeling (#1444) * Add self-label schemas * switch around array and union for self-labels * apply self-labels to post and profile views * test for self labels in pds * apply self-labels to post and profile views in bsky appview * test for self labels in bsky appview * update pds proxied test snapshots * Add support for self-labels to the mdoeration sdk * Disable unknown-labeler filtering until 3P support is added --------- Co-authored-by: Devin Ivy --- lexicons/app/bsky/actor/profile.json | 4 + lexicons/app/bsky/feed/generator.json | 6 +- lexicons/app/bsky/feed/post.json | 4 + lexicons/app/bsky/graph/list.json | 4 + lexicons/com/atproto/label/defs.json | 24 + packages/api/README.md | 14 +- packages/api/docs/moderation.md | 19 +- packages/api/src/client/lexicons.ts | 46 + .../client/types/app/bsky/actor/profile.ts | 4 + .../client/types/app/bsky/feed/generator.ts | 4 + .../src/client/types/app/bsky/feed/post.ts | 4 + .../src/client/types/app/bsky/graph/list.ts | 4 + .../client/types/com/atproto/label/defs.ts | 37 + packages/api/src/moderation/accumulator.ts | 29 +- packages/api/src/moderation/types.ts | 8 +- packages/api/tests/moderation.test.ts | 334 +++ .../api/tests/util/moderation-behavior.ts | 5 +- .../bsky/notification/listNotifications.ts | 15 +- packages/bsky/src/lexicon/lexicons.ts | 46 + .../lexicon/types/app/bsky/actor/profile.ts | 4 + .../lexicon/types/app/bsky/feed/generator.ts | 4 + .../src/lexicon/types/app/bsky/feed/post.ts | 4 + .../src/lexicon/types/app/bsky/graph/list.ts | 4 + .../lexicon/types/com/atproto/label/defs.ts | 37 + packages/bsky/src/services/actor/views.ts | 33 +- packages/bsky/src/services/feed/index.ts | 19 +- packages/bsky/src/services/feed/types.ts | 6 +- packages/bsky/src/services/feed/views.ts | 28 +- packages/bsky/src/services/label/index.ts | 23 +- .../feed-generation.test.ts.snap | 498 +++- .../tests/__snapshots__/indexing.test.ts.snap | 57 +- packages/bsky/tests/seeds/basic.ts | 7 +- packages/bsky/tests/seeds/client.ts | 12 +- packages/bsky/tests/seeds/users.ts | 6 + .../__snapshots__/author-feed.test.ts.snap | 555 +++- .../views/__snapshots__/blocks.test.ts.snap | 107 +- .../__snapshots__/mute-lists.test.ts.snap | 189 +- .../views/__snapshots__/mutes.test.ts.snap | 49 +- .../__snapshots__/notifications.test.ts.snap | 46 +- .../views/__snapshots__/posts.test.ts.snap | 176 +- .../views/__snapshots__/profile.test.ts.snap | 61 +- .../views/__snapshots__/thread.test.ts.snap | 466 +++- .../views/__snapshots__/timeline.test.ts.snap | 1602 ++++++++--- packages/bsky/tests/views/profile.test.ts | 14 + packages/bsky/tests/views/thread.test.ts | 25 + packages/bsky/tests/views/timeline.test.ts | 27 + .../bsky/notification/listNotifications.ts | 12 +- .../pds/src/app-view/services/actor/views.ts | 35 +- .../pds/src/app-view/services/feed/index.ts | 20 +- .../pds/src/app-view/services/feed/types.ts | 4 + .../pds/src/app-view/services/feed/views.ts | 28 +- .../pds/src/app-view/services/label/index.ts | 27 +- packages/pds/src/lexicon/lexicons.ts | 46 + .../lexicon/types/app/bsky/actor/profile.ts | 4 + .../lexicon/types/app/bsky/feed/generator.ts | 4 + .../src/lexicon/types/app/bsky/feed/post.ts | 4 + .../src/lexicon/types/app/bsky/graph/list.ts | 4 + .../lexicon/types/com/atproto/label/defs.ts | 37 + .../feed-generation.test.ts.snap | 606 ++++- .../tests/__snapshots__/indexing.test.ts.snap | 19 +- .../proxied/__snapshots__/admin.test.ts.snap | 44 + .../__snapshots__/feedgen.test.ts.snap | 485 +++- .../timeline-skeleton.test.ts.snap | 1027 +++++-- .../proxied/__snapshots__/views.test.ts.snap | 578 +++- packages/pds/tests/seeds/basic.ts | 7 +- packages/pds/tests/seeds/client.ts | 12 +- packages/pds/tests/seeds/users.ts | 6 + .../__snapshots__/author-feed.test.ts.snap | 770 ++++-- .../views/__snapshots__/blocks.test.ts.snap | 122 +- .../views/__snapshots__/likes.test.ts.snap | 19 +- .../__snapshots__/mute-lists.test.ts.snap | 225 +- .../__snapshots__/notifications.test.ts.snap | 508 +++- .../views/__snapshots__/posts.test.ts.snap | 217 +- .../views/__snapshots__/profile.test.ts.snap | 80 +- .../views/__snapshots__/reposts.test.ts.snap | 19 +- .../views/__snapshots__/thread.test.ts.snap | 719 ++++- .../views/__snapshots__/timeline.test.ts.snap | 2354 +++++++++++++---- .../get-moderation-action.test.ts.snap | 30 + .../get-moderation-report.test.ts.snap | 30 + .../__snapshots__/get-record.test.ts.snap | 57 + .../admin/__snapshots__/get-repo.test.ts.snap | 22 + packages/pds/tests/views/profile.test.ts | 14 + packages/pds/tests/views/thread.test.ts | 25 + packages/pds/tests/views/timeline.test.ts | 27 + 84 files changed, 10346 insertions(+), 2571 deletions(-) create mode 100644 packages/api/tests/moderation.test.ts diff --git a/lexicons/app/bsky/actor/profile.json b/lexicons/app/bsky/actor/profile.json index 02aa13d50eb..e0f476deb73 100644 --- a/lexicons/app/bsky/actor/profile.json +++ b/lexicons/app/bsky/actor/profile.json @@ -27,6 +27,10 @@ "type": "blob", "accept": ["image/png", "image/jpeg"], "maxSize": 1000000 + }, + "labels": { + "type": "union", + "refs": ["com.atproto.label.defs#selfLabels"] } } } diff --git a/lexicons/app/bsky/feed/generator.json b/lexicons/app/bsky/feed/generator.json index 558b6b7213f..78742b120f0 100644 --- a/lexicons/app/bsky/feed/generator.json +++ b/lexicons/app/bsky/feed/generator.json @@ -22,9 +22,13 @@ "accept": ["image/png", "image/jpeg"], "maxSize": 1000000 }, + "labels": { + "type": "union", + "refs": ["com.atproto.label.defs#selfLabels"] + }, "createdAt": {"type": "string", "format": "datetime"} } } } } -} \ No newline at end of file +} diff --git a/lexicons/app/bsky/feed/post.json b/lexicons/app/bsky/feed/post.json index 5f4ee579bdc..acefc29bf22 100644 --- a/lexicons/app/bsky/feed/post.json +++ b/lexicons/app/bsky/feed/post.json @@ -34,6 +34,10 @@ "maxLength": 3, "items": {"type": "string", "format": "language"} }, + "labels": { + "type": "union", + "refs": ["com.atproto.label.defs#selfLabels"] + }, "createdAt": {"type": "string", "format": "datetime"} } } diff --git a/lexicons/app/bsky/graph/list.json b/lexicons/app/bsky/graph/list.json index c17105a208e..c3583b55e14 100644 --- a/lexicons/app/bsky/graph/list.json +++ b/lexicons/app/bsky/graph/list.json @@ -22,6 +22,10 @@ "accept": ["image/png", "image/jpeg"], "maxSize": 1000000 }, + "labels": { + "type": "union", + "refs": ["com.atproto.label.defs#selfLabels"] + }, "createdAt": {"type": "string", "format": "datetime"} } } diff --git a/lexicons/com/atproto/label/defs.json b/lexicons/com/atproto/label/defs.json index bdfdeec6b67..88a262b11c8 100644 --- a/lexicons/com/atproto/label/defs.json +++ b/lexicons/com/atproto/label/defs.json @@ -37,6 +37,30 @@ "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": "#selfLabel" }, + "maxLength": 10 + } + } + }, + "selfLabel": { + "type": "object", + "description": "Metadata tag on an atproto record, published by the author within the record. Note -- 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" + } + } } } } diff --git a/packages/api/README.md b/packages/api/README.md index 8803f78e9aa..843da40459f 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -222,14 +222,22 @@ function getOpts() { // is adult content allowed? adultContentEnabled: true, - // the user's labeler settings - labelerSettings: [ + // the global label settings (used on self-labels) + labels: { + porn: 'hide', + sexual: 'warn', + nudity: 'ignore', + // ... + }, + + // the per-labeler settings + labelers: [ { labeler: { did: '...', displayName: 'My mod service' }, - settings: { + labels: { porn: 'hide', sexual: 'warn', nudity: 'ignore', diff --git a/packages/api/docs/moderation.md b/packages/api/docs/moderation.md index 1ebfe3eed77..b3d1ecc9547 100644 --- a/packages/api/docs/moderation.md +++ b/packages/api/docs/moderation.md @@ -26,14 +26,22 @@ Every moderation function takes a set of options which look like this: // is adult content allowed? adultContentEnabled: true, - // the user's labeler settings - labelerSettings: [ + // the global label settings (used on self-labels) + labels: { + porn: 'hide', + sexual: 'warn', + nudity: 'ignore', + // ... + }, + + // the per-labeler settings + labelers: [ { labeler: { did: '...', displayName: 'My mod service' }, - settings: { + labels: { porn: 'hide', sexual: 'warn', nudity: 'ignore', @@ -50,7 +58,8 @@ This should match the following interfaces: interface ModerationOpts { userDid: string adultContentEnabled: boolean - labelerSettings: LabelerSettings[] + labels: Record + labelers: LabelerSettings[] } interface Labeler { @@ -62,7 +71,7 @@ type LabelPreference = 'ignore' | 'warn' | 'hide' interface LabelerSettings { labeler: Labeler - settings: Record + labels: Record } ``` diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index ed5d0a97f81..780238d5a42 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -1441,6 +1441,36 @@ export const schemaDict = { }, }, }, + 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 -- 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: { @@ -3925,6 +3955,10 @@ export const schemaDict = { accept: ['image/png', 'image/jpeg'], maxSize: 1000000, }, + labels: { + type: 'union', + refs: ['lex:com.atproto.label.defs#selfLabels'], + }, }, }, }, @@ -4671,6 +4705,10 @@ export const schemaDict = { accept: ['image/png', 'image/jpeg'], maxSize: 1000000, }, + labels: { + type: 'union', + refs: ['lex:com.atproto.label.defs#selfLabels'], + }, createdAt: { type: 'string', format: 'datetime', @@ -5330,6 +5368,10 @@ export const schemaDict = { format: 'language', }, }, + labels: { + type: 'union', + refs: ['lex:com.atproto.label.defs#selfLabels'], + }, createdAt: { type: 'string', format: 'datetime', @@ -5949,6 +5991,10 @@ export const schemaDict = { accept: ['image/png', 'image/jpeg'], maxSize: 1000000, }, + labels: { + type: 'union', + refs: ['lex:com.atproto.label.defs#selfLabels'], + }, createdAt: { type: 'string', format: 'datetime', diff --git a/packages/api/src/client/types/app/bsky/actor/profile.ts b/packages/api/src/client/types/app/bsky/actor/profile.ts index 749a2e192b3..fa36f4298f1 100644 --- a/packages/api/src/client/types/app/bsky/actor/profile.ts +++ b/packages/api/src/client/types/app/bsky/actor/profile.ts @@ -5,12 +5,16 @@ import { ValidationResult, BlobRef } from '@atproto/lexicon' import { isObj, hasProp } from '../../../../util' import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' +import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' export interface Record { displayName?: string description?: string avatar?: BlobRef banner?: BlobRef + labels?: + | ComAtprotoLabelDefs.SelfLabels + | { $type: string; [k: string]: unknown } [k: string]: unknown } diff --git a/packages/api/src/client/types/app/bsky/feed/generator.ts b/packages/api/src/client/types/app/bsky/feed/generator.ts index 980c8fdf6fe..d9df6464f94 100644 --- a/packages/api/src/client/types/app/bsky/feed/generator.ts +++ b/packages/api/src/client/types/app/bsky/feed/generator.ts @@ -6,6 +6,7 @@ import { isObj, hasProp } from '../../../../util' import { lexicons } from '../../../../lexicons' import { CID } from 'multiformats/cid' import * as AppBskyRichtextFacet from '../richtext/facet' +import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' export interface Record { did: string @@ -13,6 +14,9 @@ export interface Record { description?: string descriptionFacets?: AppBskyRichtextFacet.Main[] avatar?: BlobRef + labels?: + | ComAtprotoLabelDefs.SelfLabels + | { $type: string; [k: string]: unknown } createdAt: string [k: string]: unknown } diff --git a/packages/api/src/client/types/app/bsky/feed/post.ts b/packages/api/src/client/types/app/bsky/feed/post.ts index f85797acaf8..1e326692640 100644 --- a/packages/api/src/client/types/app/bsky/feed/post.ts +++ b/packages/api/src/client/types/app/bsky/feed/post.ts @@ -10,6 +10,7 @@ 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 { @@ -25,6 +26,9 @@ export interface Record { | AppBskyEmbedRecordWithMedia.Main | { $type: string; [k: string]: unknown } langs?: string[] + labels?: + | ComAtprotoLabelDefs.SelfLabels + | { $type: string; [k: string]: unknown } createdAt: string [k: string]: unknown } diff --git a/packages/api/src/client/types/app/bsky/graph/list.ts b/packages/api/src/client/types/app/bsky/graph/list.ts index 0b162046b6f..4fe6dd8ed8b 100644 --- a/packages/api/src/client/types/app/bsky/graph/list.ts +++ b/packages/api/src/client/types/app/bsky/graph/list.ts @@ -7,6 +7,7 @@ import { lexicons } from '../../../../lexicons' 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 @@ -14,6 +15,9 @@ export interface Record { description?: string descriptionFacets?: AppBskyRichtextFacet.Main[] avatar?: BlobRef + labels?: + | ComAtprotoLabelDefs.SelfLabels + | { $type: string; [k: string]: unknown } createdAt: string [k: string]: unknown } diff --git a/packages/api/src/client/types/com/atproto/label/defs.ts b/packages/api/src/client/types/com/atproto/label/defs.ts index 7f76a03d3c0..13f90cd80c5 100644 --- a/packages/api/src/client/types/com/atproto/label/defs.ts +++ b/packages/api/src/client/types/com/atproto/label/defs.ts @@ -34,3 +34,40 @@ export function isLabel(v: unknown): v is 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 -- 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/api/src/moderation/accumulator.ts b/packages/api/src/moderation/accumulator.ts index ed6c06d0ddc..22a24b47119 100644 --- a/packages/api/src/moderation/accumulator.ts +++ b/packages/api/src/moderation/accumulator.ts @@ -47,15 +47,15 @@ export class ModerationCauseAccumulator { } // look up the label preference - // TODO use the find() when 3P labelers support lands - // const labelerSettings = opts.labelerSettings.find( - // (s) => s.labeler.did === label.src, - // ) - const labelerSettings = opts.labelerSettings[0] - if (!labelerSettings) { - // ignore labels from labelers we don't use - return - } + const isSelf = label.src === this.did + const labeler = isSelf + ? undefined + : opts.labelers.find((s) => s.labeler.did === label.src) + + /* TODO when 3P labelers are supported + if (!isSelf && !labeler) { + return // skip labelers not configured by the user + }*/ // establish the label preference for interpretation let labelPref: LabelPreference = 'ignore' @@ -63,8 +63,10 @@ export class ModerationCauseAccumulator { labelPref = labelDef.preferences[0] } else if (labelDef.flags.includes('adult') && !opts.adultContentEnabled) { labelPref = 'hide' - } else if (labelerSettings.settings[label.val]) { - labelPref = labelerSettings.settings[label.val] + } else if (labeler?.labels[label.val]) { + labelPref = labeler.labels[label.val] + } else if (opts.labels[label.val]) { + labelPref = opts.labels[label.val] } // ignore labels the user has asked to ignore @@ -88,9 +90,12 @@ export class ModerationCauseAccumulator { this.causes.push({ type: 'label', + source: + isSelf || !labeler + ? { type: 'user' } + : { type: 'labeler', labeler: labeler.labeler }, label, labelDef, - labeler: labelerSettings.labeler, setting: labelPref, priority, }) diff --git a/packages/api/src/moderation/types.ts b/packages/api/src/moderation/types.ts index feeeed7708f..64f39505df1 100644 --- a/packages/api/src/moderation/types.ts +++ b/packages/api/src/moderation/types.ts @@ -64,7 +64,7 @@ interface Labeler { export interface LabelerSettings { labeler: Labeler - settings: Record + labels: Record } // subjects @@ -95,13 +95,14 @@ export type ModerationSubject = export type ModerationCauseSource = | { type: 'user' } | { type: 'list'; list: AppBskyGraphDefs.ListViewBasic } + | { type: 'labeler'; labeler: Labeler } export type ModerationCause = | { type: 'blocking'; source: ModerationCauseSource; priority: 3 } | { type: 'blocked-by'; source: ModerationCauseSource; priority: 4 } | { type: 'label' - labeler: Labeler + source: ModerationCauseSource label: Label labelDef: LabelDefinition setting: LabelPreference @@ -112,7 +113,8 @@ export type ModerationCause = export interface ModerationOpts { userDid: string adultContentEnabled: boolean - labelerSettings: LabelerSettings[] + labels: Record + labelers: LabelerSettings[] } export class ModerationDecision { diff --git a/packages/api/tests/moderation.test.ts b/packages/api/tests/moderation.test.ts new file mode 100644 index 00000000000..dba625b7cfe --- /dev/null +++ b/packages/api/tests/moderation.test.ts @@ -0,0 +1,334 @@ +import { moderateProfile, moderatePost } from '../src' +import { mock } from './util' +import './util/moderation-behavior' + +describe('Moderation', () => { + it('Applies self-labels on profiles according to the global preferences', () => { + // porn (hide) + const res1 = moderateProfile( + mock.profileViewBasic({ + handle: 'bob.test', + displayName: 'Bob', + labels: [ + { + src: 'did:web:bob.test', + uri: 'at://did:web:bob.test/app.bsky.actor.profile/self', + val: 'porn', + cts: new Date().toISOString(), + }, + ], + }), + { + userDid: 'did:web:alice.test', + adultContentEnabled: true, + labels: { + porn: 'hide', + }, + labelers: [], + }, + ) + expect(res1.account).toBeModerationResult( + {}, + 'post content', + JSON.stringify(res1, null, 2), + ) + expect(res1.profile).toBeModerationResult( + {}, + 'post content', + JSON.stringify(res1, null, 2), + ) + expect(res1.avatar).toBeModerationResult( + { blur: true }, + 'post avatar', + JSON.stringify(res1, null, 2), + true, + ) + + // porn (ignore) + const res2 = moderateProfile( + mock.profileViewBasic({ + handle: 'bob.test', + displayName: 'Bob', + labels: [ + { + src: 'did:web:bob.test', + uri: 'at://did:web:bob.test/app.bsky.actor.profile/self', + val: 'porn', + cts: new Date().toISOString(), + }, + ], + }), + { + userDid: 'did:web:alice.test', + adultContentEnabled: true, + labels: { + porn: 'ignore', + }, + labelers: [], + }, + ) + expect(res2.account).toBeModerationResult( + {}, + 'post content', + JSON.stringify(res2, null, 2), + ) + expect(res2.profile).toBeModerationResult( + {}, + 'post content', + JSON.stringify(res2, null, 2), + ) + expect(res2.avatar).toBeModerationResult( + {}, + 'post avatar', + JSON.stringify(res2, null, 2), + true, + ) + }) + + it('Applies self-labels on posts according to the global preferences', () => { + // porn (hide) + const res1 = moderatePost( + mock.postView({ + record: { + text: 'Hello', + createdAt: new Date().toISOString(), + }, + author: mock.profileViewBasic({ + handle: 'bob.test', + displayName: 'Bob', + }), + labels: [ + { + src: 'did:web:bob.test', + uri: 'at://did:web:bob.test/app.bsky.actor.profile/self', + val: 'porn', + cts: new Date().toISOString(), + }, + ], + }), + { + userDid: 'did:web:alice.test', + adultContentEnabled: true, + labels: { + porn: 'hide', + }, + labelers: [], + }, + ) + expect(res1.content).toBeModerationResult( + { cause: 'label:porn', filter: true }, + 'post content', + JSON.stringify(res1, null, 2), + ) + expect(res1.embed).toBeModerationResult( + { cause: 'label:porn', blur: true }, + 'post content', + JSON.stringify(res1, null, 2), + ) + expect(res1.avatar).toBeModerationResult( + {}, + 'post avatar', + JSON.stringify(res1, null, 2), + true, + ) + + // porn (ignore) + const res2 = moderatePost( + mock.postView({ + record: { + text: 'Hello', + createdAt: new Date().toISOString(), + }, + author: mock.profileViewBasic({ + handle: 'bob.test', + displayName: 'Bob', + }), + labels: [ + { + src: 'did:web:bob.test', + uri: 'at://did:web:bob.test/app.bsky.actor.profile/self', + val: 'porn', + cts: new Date().toISOString(), + }, + ], + }), + { + userDid: 'did:web:alice.test', + adultContentEnabled: true, + labels: { + porn: 'ignore', + }, + labelers: [], + }, + ) + expect(res2.content).toBeModerationResult( + {}, + 'post content', + JSON.stringify(res2, null, 2), + ) + expect(res2.embed).toBeModerationResult( + {}, + 'post content', + JSON.stringify(res2, null, 2), + ) + expect(res2.avatar).toBeModerationResult( + {}, + 'post avatar', + JSON.stringify(res2, null, 2), + true, + ) + }) + + it('Applies labeler labels according to the per-labeler then global preferences', () => { + // porn (ignore for labeler, hide for global) + const res1 = moderateProfile( + mock.profileViewBasic({ + handle: 'bob.test', + displayName: 'Bob', + labels: [ + { + src: 'did:web:labeler.test', + uri: 'at://did:web:bob.test/app.bsky.actor.profile/self', + val: 'porn', + cts: new Date().toISOString(), + }, + ], + }), + { + userDid: 'did:web:alice.test', + adultContentEnabled: true, + labels: { + porn: 'hide', + }, + labelers: [ + { + labeler: { + did: 'did:web:labeler.test', + displayName: 'Labeler', + }, + labels: { + porn: 'ignore', + }, + }, + ], + }, + ) + expect(res1.avatar).toBeModerationResult( + {}, + 'post avatar', + JSON.stringify(res1, null, 2), + true, + ) + + // porn (hide for labeler, ignore for global) + const res2 = moderateProfile( + mock.profileViewBasic({ + handle: 'bob.test', + displayName: 'Bob', + labels: [ + { + src: 'did:web:labeler.test', + uri: 'at://did:web:bob.test/app.bsky.actor.profile/self', + val: 'porn', + cts: new Date().toISOString(), + }, + ], + }), + { + userDid: 'did:web:alice.test', + adultContentEnabled: true, + labels: { + porn: 'ignore', + }, + labelers: [ + { + labeler: { + did: 'did:web:labeler.test', + displayName: 'Labeler', + }, + labels: { + porn: 'hide', + }, + }, + ], + }, + ) + expect(res2.avatar).toBeModerationResult( + { blur: true }, + 'post avatar', + JSON.stringify(res2, null, 2), + true, + ) + + // porn (unspecified for labeler, hide for global) + const res3 = moderateProfile( + mock.profileViewBasic({ + handle: 'bob.test', + displayName: 'Bob', + labels: [ + { + src: 'did:web:labeler.test', + uri: 'at://did:web:bob.test/app.bsky.actor.profile/self', + val: 'porn', + cts: new Date().toISOString(), + }, + ], + }), + { + userDid: 'did:web:alice.test', + adultContentEnabled: true, + labels: { + porn: 'hide', + }, + labelers: [ + { + labeler: { + did: 'did:web:labeler.test', + displayName: 'Labeler', + }, + labels: {}, + }, + ], + }, + ) + expect(res3.avatar).toBeModerationResult( + { blur: true }, + 'post avatar', + JSON.stringify(res3, null, 2), + true, + ) + }) + + /* + TODO enable when 3P labeler support is addded + it('Ignores labels from unknown labelers', () => { + const res1 = moderateProfile( + mock.profileViewBasic({ + handle: 'bob.test', + displayName: 'Bob', + labels: [ + { + src: 'did:web:rando.test', + uri: 'at://did:web:bob.test/app.bsky.actor.profile/self', + val: 'porn', + cts: new Date().toISOString(), + }, + ], + }), + { + userDid: 'did:web:alice.test', + adultContentEnabled: true, + labels: { + porn: 'hide', + }, + labelers: [], + }, + ) + expect(res1.avatar).toBeModerationResult( + {}, + 'post avatar', + JSON.stringify(res1, null, 2), + true, + ) + })*/ +}) diff --git a/packages/api/tests/util/moderation-behavior.ts b/packages/api/tests/util/moderation-behavior.ts index 0d78e382c6c..4eda83a7b41 100644 --- a/packages/api/tests/util/moderation-behavior.ts +++ b/packages/api/tests/util/moderation-behavior.ts @@ -166,13 +166,14 @@ export class ModerationBehaviorSuiteRunner { adultContentEnabled: Boolean( this.suite.configurations[scenario.cfg].adultContentEnabled, ), - labelerSettings: [ + labels: this.suite.configurations[scenario.cfg].settings, + labelers: [ { labeler: { did: 'did:plc:fake-labeler', displayName: 'Fake Labeler', }, - settings: this.suite.configurations[scenario.cfg].settings, + labels: this.suite.configurations[scenario.cfg].settings, }, ], } diff --git a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts index 463b9a6f293..277a25f75cc 100644 --- a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts @@ -5,6 +5,7 @@ import { Server } from '../../../../lexicon' import { paginate, TimeCidKeyset } from '../../../../db/pagination' import AppContext from '../../../../context' import { notSoftDeletedClause } from '../../../../db/util' +import { getSelfLabels } from '../../../../services/label' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.listNotifications({ @@ -94,16 +95,26 @@ export default function (server: Server, ctx: AppContext) { const notifications = mapDefined(notifs, (notif) => { const author = authors[notif.authorDid] if (!author) return undefined + const record = jsonStringToLex(notif.recordJson) as Record< + string, + unknown + > + const recordLabels = labels[notif.uri] ?? [] + const recordSelfLabels = getSelfLabels({ + uri: notif.uri, + cid: notif.cid, + record, + }) return { uri: notif.uri, cid: notif.cid, author, reason: notif.reason, reasonSubject: notif.reasonSubject || undefined, - record: jsonStringToLex(notif.recordJson) as Record, + record, isRead: seenAt ? notif.indexedAt <= seenAt : false, indexedAt: notif.indexedAt, - labels: labels[notif.uri] ?? [], + labels: [...recordLabels, ...recordSelfLabels], } }) diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index ed5d0a97f81..780238d5a42 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -1441,6 +1441,36 @@ export const schemaDict = { }, }, }, + 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 -- 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: { @@ -3925,6 +3955,10 @@ export const schemaDict = { accept: ['image/png', 'image/jpeg'], maxSize: 1000000, }, + labels: { + type: 'union', + refs: ['lex:com.atproto.label.defs#selfLabels'], + }, }, }, }, @@ -4671,6 +4705,10 @@ export const schemaDict = { accept: ['image/png', 'image/jpeg'], maxSize: 1000000, }, + labels: { + type: 'union', + refs: ['lex:com.atproto.label.defs#selfLabels'], + }, createdAt: { type: 'string', format: 'datetime', @@ -5330,6 +5368,10 @@ export const schemaDict = { format: 'language', }, }, + labels: { + type: 'union', + refs: ['lex:com.atproto.label.defs#selfLabels'], + }, createdAt: { type: 'string', format: 'datetime', @@ -5949,6 +5991,10 @@ export const schemaDict = { accept: ['image/png', 'image/jpeg'], maxSize: 1000000, }, + labels: { + type: 'union', + refs: ['lex:com.atproto.label.defs#selfLabels'], + }, createdAt: { type: 'string', format: 'datetime', diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/profile.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/profile.ts index 2d4bf526601..7dbc4c1ccec 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/profile.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/profile.ts @@ -5,12 +5,16 @@ 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 } diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/generator.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/generator.ts index 5ad34318ee3..757e74db845 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/generator.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/generator.ts @@ -6,6 +6,7 @@ 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 @@ -13,6 +14,9 @@ export interface Record { description?: string descriptionFacets?: AppBskyRichtextFacet.Main[] avatar?: BlobRef + labels?: + | ComAtprotoLabelDefs.SelfLabels + | { $type: string; [k: string]: unknown } createdAt: string [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts index b111c4783e6..8942bc724cd 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/post.ts @@ -10,6 +10,7 @@ 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 { @@ -25,6 +26,9 @@ export interface Record { | AppBskyEmbedRecordWithMedia.Main | { $type: string; [k: string]: unknown } langs?: string[] + labels?: + | ComAtprotoLabelDefs.SelfLabels + | { $type: string; [k: string]: unknown } createdAt: string [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/list.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/list.ts index 4304ca98b03..36a7fb17a3f 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/list.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/list.ts @@ -7,6 +7,7 @@ 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 @@ -14,6 +15,9 @@ export interface Record { description?: string descriptionFacets?: AppBskyRichtextFacet.Main[] avatar?: BlobRef + labels?: + | ComAtprotoLabelDefs.SelfLabels + | { $type: string; [k: string]: unknown } createdAt: string [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/com/atproto/label/defs.ts b/packages/bsky/src/lexicon/types/com/atproto/label/defs.ts index 17a8480b9d6..a01ad78e254 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/label/defs.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/label/defs.ts @@ -34,3 +34,40 @@ export function isLabel(v: unknown): v is 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 -- 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/bsky/src/services/actor/views.ts b/packages/bsky/src/services/actor/views.ts index be08c0a3328..1313b0a66a6 100644 --- a/packages/bsky/src/services/actor/views.ts +++ b/packages/bsky/src/services/actor/views.ts @@ -1,5 +1,6 @@ import { mapDefined } from '@atproto/common' import { INVALID_HANDLE } from '@atproto/identifier' +import { jsonStringToLex } from '@atproto/lexicon' import { ProfileViewDetailed, ProfileView, @@ -9,7 +10,7 @@ import Database from '../../db' import { noMatch, notSoftDeletedClause } from '../../db/util' import { Actor } from '../../db/tables/actor' import { ImageUriBuilder } from '../../image/uri' -import { LabelService } from '../label' +import { LabelService, getSelfLabels } from '../label' import { GraphService } from '../graph' import { LabelCache } from '../../label-cache' @@ -41,6 +42,7 @@ export class ActorViews { .where('actor.did', 'in', dids) .leftJoin('profile', 'profile.creator', 'actor.did') .leftJoin('profile_agg', 'profile_agg.did', 'actor.did') + .leftJoin('record', 'record.uri', 'profile.uri') .if(!includeSoftDeleted, (qb) => qb.where(notSoftDeletedClause(ref('actor'))), ) @@ -48,6 +50,7 @@ export class ActorViews { 'actor.did as did', 'actor.handle as handle', 'profile.uri as profileUri', + 'profile.cid as profileCid', 'profile.displayName as displayName', 'profile.description as description', 'profile.avatarCid as avatarCid', @@ -56,6 +59,7 @@ export class ActorViews { 'profile_agg.followsCount as followsCount', 'profile_agg.followersCount as followersCount', 'profile_agg.postsCount as postsCount', + 'record.json as profileJson', this.db.db .selectFrom('follow') .if(!viewer, (q) => q.where(noMatch)) @@ -113,7 +117,6 @@ export class ActorViews { const listViews = await this.services.graph.getListViews(listUris, viewer) return profileInfos.reduce((acc, cur) => { - const actorLabels = labels[cur.did] ?? [] const avatar = cur?.avatarCid ? this.imgUriBuilder.getPresetUri('avatar', cur.did, cur.avatarCid) : undefined @@ -126,6 +129,15 @@ export class ActorViews { listViews[cur.requesterMutedByList], ) : undefined + const actorLabels = labels[cur.did] ?? [] + const selfLabels = getSelfLabels({ + uri: cur.profileUri, + cid: cur.profileCid, + record: + cur.profileJson !== null + ? (jsonStringToLex(cur.profileJson) as Record) + : null, + }) const profile = { did: cur.did, handle: cur.handle ?? INVALID_HANDLE, @@ -147,7 +159,7 @@ export class ActorViews { blocking: cur.requesterBlocking || undefined, } : undefined, - labels: skipLabels ? undefined : actorLabels, + labels: skipLabels ? undefined : [...actorLabels, ...selfLabels], } acc[cur.did] = profile return acc @@ -187,6 +199,7 @@ export class ActorViews { .selectFrom('actor') .where('actor.did', 'in', dids) .leftJoin('profile', 'profile.creator', 'actor.did') + .leftJoin('record', 'record.uri', 'profile.uri') .if(!includeSoftDeleted, (qb) => qb.where(notSoftDeletedClause(ref('actor'))), ) @@ -194,10 +207,12 @@ export class ActorViews { 'actor.did as did', 'actor.handle as handle', 'profile.uri as profileUri', + 'profile.cid as profileCid', 'profile.displayName as displayName', 'profile.description as description', 'profile.avatarCid as avatarCid', 'profile.indexedAt as indexedAt', + 'record.json as profileJson', this.db.db .selectFrom('follow') .if(!viewer, (q) => q.where(noMatch)) @@ -255,7 +270,6 @@ export class ActorViews { const listViews = await this.services.graph.getListViews(listUris, viewer) return profileInfos.reduce((acc, cur) => { - const actorLabels = labels[cur.did] ?? [] const avatar = cur?.avatarCid ? this.imgUriBuilder.getPresetUri('avatar', cur.did, cur.avatarCid) : undefined @@ -265,6 +279,15 @@ export class ActorViews { listViews[cur.requesterMutedByList], ) : undefined + const actorLabels = labels[cur.did] ?? [] + const selfLabels = getSelfLabels({ + uri: cur.profileUri, + cid: cur.profileCid, + record: + cur.profileJson !== null + ? (jsonStringToLex(cur.profileJson) as Record) + : null, + }) const profile = { did: cur.did, handle: cur.handle ?? INVALID_HANDLE, @@ -282,7 +305,7 @@ export class ActorViews { followedBy: cur?.requesterFollowedBy || undefined, } : undefined, - labels: skipLabels ? undefined : actorLabels, + labels: skipLabels ? undefined : [...actorLabels, ...selfLabels], } acc[cur.did] = profile return acc diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index 449ff8d7e88..0d71be81aa9 100644 --- a/packages/bsky/src/services/feed/index.ts +++ b/packages/bsky/src/services/feed/index.ts @@ -39,8 +39,9 @@ import { PostInfo, RecordEmbedViewRecord, PostBlocksMap, + kSelfLabels, } from './types' -import { LabelService, Labels } from '../label' +import { LabelService, Labels, getSelfLabels } from '../label' import { ActorService } from '../actor' import { GraphService } from '../graph' import { FeedViews } from './views' @@ -136,15 +137,18 @@ export class FeedService { this.db.db .selectFrom('actor') .leftJoin('profile', 'profile.creator', 'actor.did') + .leftJoin('record', 'record.uri', 'profile.uri') .where('actor.did', 'in', dids) .where(notSoftDeletedClause(ref('actor'))) .selectAll('actor') .select([ 'profile.uri as profileUri', + 'profile.cid as profileCid', 'profile.displayName as displayName', 'profile.description as description', 'profile.avatarCid as avatarCid', 'profile.indexedAt as indexedAt', + 'record.json as profileJson', this.db.db .selectFrom('follow') .if(!viewer, (q) => q.where(noMatch)) @@ -198,7 +202,6 @@ export class FeedService { .filter((list) => !!list) const listViews = await this.services.graph.getListViews(listUris, viewer) return actors.reduce((acc, cur) => { - const actorLabels = labels[cur.did] ?? [] const avatar = cur.avatarCid ? this.imgUriBuilder.getPresetUri('avatar', cur.did, cur.avatarCid) : undefined @@ -208,6 +211,15 @@ export class FeedService { listViews[cur.requesterMutedByList], ) : undefined + const actorLabels = labels[cur.did] ?? [] + const selfLabels = getSelfLabels({ + uri: cur.profileUri, + cid: cur.profileCid, + record: + cur.profileJson !== null + ? (jsonStringToLex(cur.profileJson) as Record) + : null, + }) return { ...acc, [cur.did]: { @@ -225,7 +237,8 @@ export class FeedService { followedBy: cur?.requesterFollowedBy || undefined, } : undefined, - labels: skipLabels ? undefined : actorLabels, + labels: skipLabels ? undefined : [...actorLabels, ...selfLabels], + [kSelfLabels]: selfLabels, }, } }, {} as ActorInfoMap) diff --git a/packages/bsky/src/services/feed/types.ts b/packages/bsky/src/services/feed/types.ts index b32f2a646be..f1b5500c62e 100644 --- a/packages/bsky/src/services/feed/types.ts +++ b/packages/bsky/src/services/feed/types.ts @@ -1,3 +1,4 @@ +import { Selectable } from 'kysely' import { View as ImagesEmbedView } from '../../lexicon/types/app/bsky/embed/images' import { View as ExternalEmbedView } from '../../lexicon/types/app/bsky/embed/external' import { @@ -16,7 +17,6 @@ import { import { Label } from '../../lexicon/types/com/atproto/label/defs' import { FeedGenerator } from '../../db/tables/feed-generator' import { ListView } from '../../lexicon/types/app/bsky/graph/defs' -import { Selectable } from 'kysely' export type PostEmbedViews = { [uri: string]: PostEmbedView @@ -50,6 +50,8 @@ export type PostBlocksMap = { [uri: string]: { reply?: boolean; embed?: boolean } } +export const kSelfLabels = Symbol('selfLabels') + export type ActorInfo = { did: string handle: string @@ -63,6 +65,8 @@ export type ActorInfo = { followedBy?: string } labels?: Label[] + // allows threading self-labels through if they are going to be applied later, i.e. when using skipLabels option. + [kSelfLabels]?: Label[] } export type ActorInfoMap = { [did: string]: ActorInfo } diff --git a/packages/bsky/src/services/feed/views.ts b/packages/bsky/src/services/feed/views.ts index d366ab0d3d3..dde89d5762e 100644 --- a/packages/bsky/src/services/feed/views.ts +++ b/packages/bsky/src/services/feed/views.ts @@ -29,9 +29,9 @@ import { PostInfoMap, RecordEmbedViewRecord, PostBlocksMap, + kSelfLabels, } from './types' -import { Labels } from '../label' -import { ProfileView } from '../../lexicon/types/app/bsky/actor/defs' +import { Labels, getSelfLabels } from '../label' import { ImageUriBuilder } from '../../image/uri' export class FeedViews { @@ -43,14 +43,16 @@ export class FeedViews { formatFeedGeneratorView( info: FeedGenInfo, - profiles: Record, + profiles: ActorInfoMap, labels?: Labels, ): GeneratorView { const profile = profiles[info.creator] - if (profile) { + if (profile && !profile.labels) { // If the creator labels are not hydrated yet, attempt to pull them // from labels: e.g. compatible with embedsForPosts() batching label hydration. - profile.labels ??= labels?.[info.creator] ?? [] + const profileLabels = labels?.[info.creator] ?? [] + const profileSelfLabels = profile[kSelfLabels] ?? [] + profile.labels = [...profileLabels, ...profileSelfLabels] } return { uri: info.uri, @@ -108,11 +110,13 @@ export class FeedViews { if (!originator) { continue } else { + const originatorLabels = labels[item.originatorDid] ?? [] + const originatorSelfLabels = originator[kSelfLabels] ?? [] feedPost['reason'] = { $type: 'app.bsky.feed.defs#reasonRepost', by: { ...originator, - labels: labels[item.originatorDid] ?? [], + labels: [...originatorLabels, ...originatorSelfLabels], }, indexedAt: item.sortAt, } @@ -161,7 +165,15 @@ export class FeedViews { if (!post || !author) return undefined // If the author labels are not hydrated yet, attempt to pull them // from labels: e.g. compatible with hydrateFeed() batching label hydration. - author.labels ??= labels[author.did] ?? [] + const authorLabels = labels[author.did] ?? [] + const authorSelfLabels = author[kSelfLabels] ?? [] + author.labels ??= [...authorLabels, ...authorSelfLabels] + const postLabels = labels[uri] ?? [] + const postSelfLabels = getSelfLabels({ + uri: post.uri, + cid: post.cid, + record: post.record, + }) return { uri: post.uri, cid: post.cid, @@ -178,7 +190,7 @@ export class FeedViews { like: post.requesterLike ?? undefined, } : undefined, - labels: labels[uri] ?? [], + labels: [...postLabels, ...postSelfLabels], } } diff --git a/packages/bsky/src/services/label/index.ts b/packages/bsky/src/services/label/index.ts index c5090b346fc..9d80c2fca53 100644 --- a/packages/bsky/src/services/label/index.ts +++ b/packages/bsky/src/services/label/index.ts @@ -1,8 +1,9 @@ +import { sql } from 'kysely' import { AtUri } from '@atproto/uri' import Database from '../../db' -import { Label } from '../../lexicon/types/com/atproto/label/defs' +import { Label, isSelfLabels } from '../../lexicon/types/com/atproto/label/defs' import { ids } from '../../lexicon/lexicons' -import { sql } from 'kysely' +import { toSimplifiedISOSafe } from '../indexing/util' import { LabelCache } from '../../label-cache' export type Labels = Record @@ -148,3 +149,21 @@ export class LabelService { return labels[did] ?? [] } } + +export function getSelfLabels(details: { + uri: string | null + cid: string | null + record: Record | null +}): Label[] { + const { uri, cid, record } = details + if (!uri || !cid || !record) return [] + if (!isSelfLabels(record.labels)) return [] + const src = new AtUri(uri).host // record creator + const cts = + typeof record.createdAt === 'string' + ? toSimplifiedISOSafe(record.createdAt) + : new Date(0).toISOString() + return record.labels.values.map(({ val }) => { + return { src, uri, cid, val, cts, neg: false } + }) +} diff --git a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap index a3b4e35a342..1c5206c28de 100644 --- a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap +++ b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap @@ -67,7 +67,24 @@ Object { "did": "user(3)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(4)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(4)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(3)", @@ -82,7 +99,7 @@ Object { "likeCount": 2, "uri": "record(1)", "viewer": Object { - "like": "record(4)", + "like": "record(5)", }, }, }, @@ -119,7 +136,24 @@ Array [ "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -136,7 +170,7 @@ Array [ "viewer": Object {}, }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", "description": "its me!", @@ -144,7 +178,24 @@ Array [ "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -157,11 +208,11 @@ Array [ "displayName": "Bad Pagination", "indexedAt": "1970-01-01T00:00:00.000Z", "likeCount": 0, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object {}, }, Object { - "cid": "cids(3)", + "cid": "cids(4)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", "description": "its me!", @@ -169,7 +220,24 @@ Array [ "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -182,11 +250,11 @@ Array [ "displayName": "Even", "indexedAt": "1970-01-01T00:00:00.000Z", "likeCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, Object { - "cid": "cids(4)", + "cid": "cids(5)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", "description": "its me!", @@ -194,7 +262,24 @@ Array [ "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -207,9 +292,9 @@ Array [ "displayName": "All", "indexedAt": "1970-01-01T00:00:00.000Z", "likeCount": 2, - "uri": "record(5)", + "uri": "record(6)", "viewer": Object { - "like": "record(6)", + "like": "record(7)", }, }, ] @@ -224,7 +309,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -232,11 +334,28 @@ Array [ }, "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, @@ -255,12 +374,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -275,7 +394,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -287,12 +406,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(7)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -300,13 +419,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/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", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(5)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "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", + "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", }, ], }, @@ -321,16 +440,16 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embeds": Array [], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(1)", + "uri": "record(2)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -360,7 +479,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -371,7 +490,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(6)", }, "size": 12736, }, @@ -380,8 +499,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(2)", - "uri": "record(1)", + "cid": "cids(3)", + "uri": "record(2)", }, }, }, @@ -389,9 +508,9 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object { - "like": "record(7)", + "like": "record(8)", }, }, }, @@ -403,12 +522,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(7)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -417,19 +536,19 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(7)", - "uri": "record(9)", + "cid": "cids(8)", + "uri": "record(10)", }, "root": Object { - "cid": "cids(7)", - "uri": "record(9)", + "cid": "cids(8)", + "uri": "record(10)", }, }, "text": "of course", }, "replyCount": 0, "repostCount": 0, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object {}, }, "reply": Object { @@ -440,13 +559,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -457,7 +593,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(9)", + "uri": "record(10)", "viewer": Object {}, }, "root": Object { @@ -467,13 +603,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -484,7 +637,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(9)", + "uri": "record(10)", "viewer": Object {}, }, }, @@ -497,11 +650,11 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(11)", + "following": "record(12)", "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -512,12 +665,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(7)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -526,13 +679,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/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", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(5)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "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", + "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", }, ], }, @@ -547,15 +700,15 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(1)", + "uri": "record(2)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -571,7 +724,7 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(4)", + "uri": "record(5)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -586,7 +739,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -597,7 +750,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(6)", }, "size": 12736, }, @@ -606,8 +759,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(2)", - "uri": "record(1)", + "cid": "cids(3)", + "uri": "record(2)", }, }, }, @@ -624,8 +777,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(3)", - "uri": "record(4)", + "cid": "cids(4)", + "uri": "record(5)", }, }, "facets": Array [ @@ -646,7 +799,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(10)", + "uri": "record(11)", "viewer": Object {}, }, "reason": Object { @@ -657,8 +810,8 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(7)", + "following": "record(6)", "muted": false, }, }, @@ -677,7 +830,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -685,11 +855,28 @@ Array [ }, "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, @@ -706,12 +893,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -719,13 +906,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.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", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(4)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(4)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "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", + "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", }, ], }, @@ -740,16 +927,16 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(7)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "embeds": Array [], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(4)", + "uri": "record(5)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -779,7 +966,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -790,7 +977,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 12736, }, @@ -799,8 +986,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(5)", - "uri": "record(4)", + "cid": "cids(6)", + "uri": "record(5)", }, }, }, @@ -808,9 +995,9 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object { - "like": "record(7)", + "like": "record(8)", }, }, }, @@ -822,12 +1009,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -836,19 +1023,19 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(7)", - "uri": "record(9)", + "cid": "cids(8)", + "uri": "record(10)", }, "root": Object { - "cid": "cids(7)", - "uri": "record(9)", + "cid": "cids(8)", + "uri": "record(10)", }, }, "text": "of course", }, "replyCount": 0, "repostCount": 0, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object {}, }, "reply": Object { @@ -859,13 +1046,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -876,7 +1080,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(9)", + "uri": "record(10)", "viewer": Object {}, }, "root": Object { @@ -886,13 +1090,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -903,7 +1124,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(9)", + "uri": "record(10)", "viewer": Object {}, }, }, @@ -922,7 +1143,24 @@ Object { "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -937,7 +1175,7 @@ Object { "likeCount": 2, "uri": "record(0)", "viewer": Object { - "like": "record(3)", + "like": "record(4)", }, }, } @@ -953,7 +1191,24 @@ Object { "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -968,17 +1223,34 @@ Object { "likeCount": 2, "uri": "record(0)", "viewer": Object { - "like": "record(3)", + "like": "record(4)", }, }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -991,7 +1263,7 @@ Object { "displayName": "Even", "indexedAt": "1970-01-01T00:00:00.000Z", "likeCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, ], diff --git a/packages/bsky/tests/__snapshots__/indexing.test.ts.snap b/packages/bsky/tests/__snapshots__/indexing.test.ts.snap index 6f48e290911..8fb7e5cf193 100644 --- a/packages/bsky/tests/__snapshots__/indexing.test.ts.snap +++ b/packages/bsky/tests/__snapshots__/indexing.test.ts.snap @@ -380,11 +380,28 @@ Array [ }, "cid": "cids(11)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(11)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(12)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, @@ -449,7 +466,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -498,7 +532,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, diff --git a/packages/bsky/tests/seeds/basic.ts b/packages/bsky/tests/seeds/basic.ts index 5c22b2da65d..d0443e04d54 100644 --- a/packages/bsky/tests/seeds/basic.ts +++ b/packages/bsky/tests/seeds/basic.ts @@ -23,7 +23,12 @@ export default async (sc: SeedClient, users = true) => { await sc.follow(bob, alice) await sc.follow(bob, carol, createdAtMicroseconds()) await sc.follow(dan, bob, createdAtTimezone()) - await sc.post(alice, posts.alice[0]) + await sc.post(alice, posts.alice[0], undefined, undefined, undefined, { + labels: { + $type: 'com.atproto.label.defs#selfLabels', + values: [{ val: 'self-label' }], + }, + }) await sc.post(bob, posts.bob[0], undefined, undefined, undefined, { langs: ['en-US', 'i-klingon'], }) diff --git a/packages/bsky/tests/seeds/client.ts b/packages/bsky/tests/seeds/client.ts index ea4167cdbd2..946920b202d 100644 --- a/packages/bsky/tests/seeds/client.ts +++ b/packages/bsky/tests/seeds/client.ts @@ -117,7 +117,7 @@ export class SeedClient { by: string, displayName: string, description: string, - fromUser?: string, + selfLabels?: string[], ) { AVATAR_IMG ??= await fs.readFile( 'tests/image/fixtures/key-portrait-small.jpg', @@ -127,7 +127,7 @@ export class SeedClient { { const res = await this.agent.api.com.atproto.repo.uploadBlob(AVATAR_IMG, { encoding: 'image/jpeg', - headers: this.getHeaders(fromUser || by), + headers: this.getHeaders(by), } as any) avatarBlob = res.data.blob } @@ -139,8 +139,14 @@ export class SeedClient { displayName, description, avatar: avatarBlob, + labels: selfLabels + ? { + $type: 'com.atproto.label.defs#selfLabels', + values: selfLabels.map((val) => ({ val })), + } + : undefined, }, - this.getHeaders(fromUser || by), + this.getHeaders(by), ) this.profiles[by] = { displayName, diff --git a/packages/bsky/tests/seeds/users.ts b/packages/bsky/tests/seeds/users.ts index 0a7d530d998..8c14b894db4 100644 --- a/packages/bsky/tests/seeds/users.ts +++ b/packages/bsky/tests/seeds/users.ts @@ -10,11 +10,13 @@ export default async (sc: SeedClient) => { sc.dids.alice, users.alice.displayName, users.alice.description, + users.alice.selfLabels, ) await sc.createProfile( sc.dids.bob, users.bob.displayName, users.bob.description, + users.bob.selfLabels, ) return sc @@ -27,6 +29,7 @@ const users = { password: 'alice-pass', displayName: 'ali', description: 'its me!', + selfLabels: ['self-label-a', 'self-label-b'], }, bob: { email: 'bob@test.com', @@ -34,6 +37,7 @@ const users = { password: 'bob-pass', displayName: 'bobby', description: 'hi im bob label_me', + selfLabels: undefined, }, carol: { email: 'carol@test.com', @@ -41,6 +45,7 @@ const users = { password: 'carol-pass', displayName: undefined, description: undefined, + selfLabels: undefined, }, dan: { email: 'dan@test.com', @@ -48,5 +53,6 @@ const users = { password: 'dan-pass', displayName: undefined, description: undefined, + selfLabels: undefined, }, } 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 e66085e7096..67ee62e1336 100644 --- a/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap @@ -9,7 +9,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -24,12 +41,12 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(3)", - "uri": "record(2)", + "cid": "cids(4)", + "uri": "record(3)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(1)", + "cid": "cids(3)", + "uri": "record(2)", }, }, "text": "thanks bob", @@ -50,38 +67,38 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)@jpeg", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(2)", + "uri": "record(3)", "val": "test-label", }, Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(2)", + "uri": "record(3)", "val": "test-label-2", }, ], @@ -98,7 +115,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -107,19 +124,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(1)", + "cid": "cids(3)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(1)", + "cid": "cids(3)", + "uri": "record(2)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, "root": Object { @@ -129,13 +146,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -146,7 +180,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -158,13 +192,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -175,11 +226,11 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(7)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", @@ -191,15 +242,15 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(8)", + "uri": "record(9)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -214,7 +265,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -225,7 +276,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(8)", + "$link": "cids(9)", }, "size": 12736, }, @@ -234,8 +285,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(9)", - "uri": "record(11)", + "cid": "cids(10)", + "uri": "record(12)", }, }, }, @@ -246,15 +297,15 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(6)", + "uri": "record(7)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(7)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(9)", }, }, "facets": Array [ @@ -278,11 +329,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(5)", + "uri": "record(6)", "val": "test-label", }, ], @@ -293,15 +344,15 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(6)", - "uri": "record(6)", + "cid": "cids(7)", + "uri": "record(7)", }, }, "text": "yoohoo label_me", }, "replyCount": 0, "repostCount": 0, - "uri": "record(5)", + "uri": "record(6)", "viewer": Object {}, }, }, @@ -312,13 +363,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -329,7 +397,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -340,24 +408,58 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(11)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(11)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(13)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(12)", + "uri": "record(13)", "viewer": Object {}, }, }, @@ -454,7 +556,24 @@ Array [ "did": "user(2)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(4)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(4)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(3)", @@ -475,7 +594,7 @@ Array [ "repostCount": 1, "uri": "record(1)", "viewer": Object { - "like": "record(4)", + "like": "record(5)", }, }, "root": Object { @@ -485,7 +604,24 @@ Array [ "did": "user(2)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(4)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(4)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(3)", @@ -506,7 +642,7 @@ Array [ "repostCount": 1, "uri": "record(1)", "viewer": Object { - "like": "record(4)", + "like": "record(5)", }, }, }, @@ -524,7 +660,7 @@ Array [ "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(5)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -535,7 +671,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(5)", + "uri": "record(6)", "viewer": Object {}, }, }, @@ -552,7 +688,7 @@ Array [ "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -567,7 +703,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(6)", + "uri": "record(7)", "viewer": Object {}, }, }, @@ -791,7 +927,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(8)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(8)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(8)", @@ -812,7 +965,7 @@ Array [ "repostCount": 1, "uri": "record(6)", "viewer": Object { - "like": "record(9)", + "like": "record(10)", }, }, "root": Object { @@ -822,7 +975,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(8)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(8)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(8)", @@ -843,7 +1013,7 @@ Array [ "repostCount": 1, "uri": "record(6)", "viewer": Object { - "like": "record(9)", + "like": "record(10)", }, }, }, @@ -971,7 +1141,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -991,8 +1178,8 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", - "repost": "record(2)", + "like": "record(4)", + "repost": "record(3)", }, }, "reason": Object { @@ -1020,7 +1207,7 @@ Array [ "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -1034,7 +1221,7 @@ Array [ "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -1043,13 +1230,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "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", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)@jpeg", }, ], }, @@ -1064,14 +1251,14 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(7)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(6)", + "uri": "record(7)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1087,7 +1274,7 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(5)", + "uri": "record(6)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1102,7 +1289,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1113,7 +1300,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(6)", }, "size": 12736, }, @@ -1122,8 +1309,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(6)", - "uri": "record(6)", + "cid": "cids(7)", + "uri": "record(7)", }, }, }, @@ -1140,8 +1327,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(3)", - "uri": "record(5)", + "cid": "cids(4)", + "uri": "record(6)", }, }, "facets": Array [ @@ -1162,7 +1349,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, }, @@ -1177,7 +1364,7 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1188,7 +1375,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object {}, }, }, @@ -1204,7 +1391,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1221,12 +1425,12 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(3)", - "uri": "record(4)", + "cid": "cids(4)", + "uri": "record(5)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "text": "thanks bob", @@ -1247,37 +1451,37 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", + "followedBy": "record(7)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)@jpeg", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label", }, Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label-2", }, ], @@ -1294,7 +1498,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1303,19 +1507,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "root": Object { @@ -1325,7 +1529,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1333,7 +1554,7 @@ Array [ "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1344,9 +1565,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object { - "like": "record(5)", + "like": "record(6)", }, }, }, @@ -1358,7 +1579,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1366,7 +1604,7 @@ Array [ "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -1380,7 +1618,7 @@ Array [ "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", @@ -1395,10 +1633,10 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(9)", + "uri": "record(10)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1413,7 +1651,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1424,7 +1662,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(8)", + "$link": "cids(9)", }, "size": 12736, }, @@ -1433,8 +1671,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(9)", - "uri": "record(10)", + "cid": "cids(10)", + "uri": "record(11)", }, }, }, @@ -1445,15 +1683,15 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(8)", + "uri": "record(9)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(7)", - "uri": "record(9)", + "cid": "cids(8)", + "uri": "record(10)", }, }, "facets": Array [ @@ -1477,11 +1715,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(7)", + "uri": "record(8)", "val": "test-label", }, ], @@ -1492,17 +1730,17 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(7)", + "uri": "record(9)", }, }, "text": "yoohoo label_me", }, "replyCount": 0, "repostCount": 0, - "uri": "record(7)", + "uri": "record(8)", "viewer": Object { - "like": "record(11)", + "like": "record(12)", }, }, }, @@ -1513,7 +1751,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1521,7 +1776,7 @@ Array [ "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1532,9 +1787,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object { - "like": "record(5)", + "like": "record(6)", }, }, }, @@ -1545,7 +1800,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1553,18 +1825,35 @@ Array [ "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(11)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(11)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(13)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(12)", + "uri": "record(13)", "viewer": Object {}, }, }, diff --git a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap index 57688462432..812959c5497 100644 --- a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap @@ -10,7 +10,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -31,27 +48,27 @@ Object { "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewBlocked", - "uri": "record(3)", + "uri": "record(4)", }, }, ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(2)", + "uri": "record(3)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(3)", - "uri": "record(3)", + "cid": "cids(4)", + "uri": "record(4)", }, }, "facets": Array [ @@ -90,8 +107,8 @@ Object { "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(2)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(3)", }, }, "text": "yoohoo label_me", @@ -112,7 +129,7 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#blockedPost", "blocked": true, - "uri": "record(3)", + "uri": "record(4)", }, "post": Object { "author": Object { @@ -120,7 +137,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -137,12 +171,12 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "text": "alice replies to dan", @@ -167,7 +201,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -187,15 +238,15 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", - "repost": "record(2)", + "like": "record(4)", + "repost": "record(3)", }, }, "replies": Array [ Object { "$type": "app.bsky.feed.defs#blockedPost", "blocked": true, - "uri": "record(4)", + "uri": "record(5)", }, Object { "$type": "app.bsky.feed.defs#threadViewPost", @@ -208,37 +259,37 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(6)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)@jpeg", + "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", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(5)", + "uri": "record(6)", "val": "test-label", }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(5)", + "uri": "record(6)", "val": "test-label-2", }, ], @@ -255,7 +306,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -276,7 +327,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(5)", + "uri": "record(6)", "viewer": Object {}, }, }, diff --git a/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap b/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap index 97cc1b2ad6e..748180f4d57 100644 --- a/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap @@ -7,7 +7,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -18,7 +35,7 @@ Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.graph.defs#listView", - "cid": "cids(3)", + "cid": "cids(4)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "did": "user(0)", @@ -33,7 +50,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "name": "updated alice mutes", "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(1)", + "uri": "record(2)", "viewer": Object { "muted": false, }, @@ -48,8 +65,8 @@ Object { "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(2)", - "uri": "record(1)", + "cid": "cids(3)", + "uri": "record(2)", }, }, "text": "list embed!", @@ -70,7 +87,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -90,8 +124,8 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", - "repost": "record(2)", + "like": "record(4)", + "repost": "record(3)", }, }, "replies": Array [ @@ -104,23 +138,23 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": true, "mutedByList": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", - "cid": "cids(3)", + "cid": "cids(4)", "indexedAt": "1970-01-01T00:00:00.000Z", "name": "alice mutes", "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(5)", + "uri": "record(6)", "viewer": Object { "muted": true, }, }, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -141,7 +175,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, }, @@ -156,48 +190,48 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(9)", + "following": "record(10)", "muted": true, "mutedByList": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", - "cid": "cids(3)", + "cid": "cids(4)", "indexedAt": "1970-01-01T00:00:00.000Z", "name": "alice mutes", "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(5)", + "uri": "record(6)", "viewer": Object { "muted": true, }, }, }, }, - "cid": "cids(4)", + "cid": "cids(5)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)@jpeg", + "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", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(4)", + "cid": "cids(5)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(8)", + "uri": "record(9)", "val": "test-label", }, Object { - "cid": "cids(4)", + "cid": "cids(5)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(8)", + "uri": "record(9)", "val": "test-label-2", }, ], @@ -214,7 +248,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(6)", }, "size": 4114, }, @@ -235,7 +269,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object {}, }, }, @@ -256,7 +290,24 @@ Object { "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -274,7 +325,7 @@ Object { }, Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", - "cid": "cids(2)", + "cid": "cids(3)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "description": "its me!", @@ -282,7 +333,24 @@ Object { "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -293,7 +361,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "name": "alice mutes", "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(2)", + "uri": "record(3)", "viewer": Object { "muted": true, }, @@ -315,7 +383,24 @@ Object { "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -333,7 +418,7 @@ Object { }, Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", - "cid": "cids(2)", + "cid": "cids(3)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "description": "its me!", @@ -341,7 +426,24 @@ Object { "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -352,7 +454,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "name": "alice mutes", "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(2)", + "uri": "record(3)", "viewer": Object { "muted": true, }, @@ -426,7 +528,24 @@ Object { "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(4)", + "uri": "record(4)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(4)", + "uri": "record(4)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(3)", diff --git a/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap b/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap index 181022cc578..d6836a810a5 100644 --- a/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/mutes.test.ts.snap @@ -107,7 +107,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -137,12 +154,12 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(4)", + "following": "record(3)", "muted": true, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -163,7 +180,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -178,38 +195,38 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(7)", + "following": "record(6)", "muted": true, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(4)@jpeg", + "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", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label", }, Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label-2", }, ], @@ -226,7 +243,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -247,7 +264,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, }, diff --git a/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap b/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap index 8b4bc79de25..fd8c6b0ff3c 100644 --- a/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap @@ -791,7 +791,24 @@ Array [ "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -818,23 +835,40 @@ Array [ "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(5)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [ Object { - "cid": "cids(4)", + "cid": "cids(5)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(3)", + "uri": "record(4)", "val": "test-label", }, ], @@ -852,7 +886,7 @@ Array [ }, "text": "yoohoo label_me", }, - "uri": "record(3)", + "uri": "record(4)", }, ] `; diff --git a/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap b/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap index 75392f67e85..5e9e61e52ad 100644 --- a/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap @@ -8,7 +8,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -16,11 +33,28 @@ Array [ }, "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, @@ -34,13 +68,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -51,7 +102,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, Object { @@ -63,12 +114,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -83,7 +134,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, Object { @@ -93,12 +144,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(5)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -106,13 +157,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/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", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(6)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(6)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "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", + "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", }, ], }, @@ -127,16 +178,16 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embeds": Array [], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(2)", + "uri": "record(3)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -166,7 +217,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(6)", }, "size": 4114, }, @@ -177,7 +228,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(6)", + "$link": "cids(7)", }, "size": 12736, }, @@ -186,8 +237,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(3)", - "uri": "record(2)", + "cid": "cids(4)", + "uri": "record(3)", }, }, }, @@ -195,9 +246,9 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(5)", + "uri": "record(6)", "viewer": Object { - "like": "record(8)", + "like": "record(9)", }, }, Object { @@ -207,11 +258,11 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(10)", + "following": "record(11)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -222,12 +273,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(5)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -236,13 +287,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/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", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(6)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(6)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "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", + "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", }, ], }, @@ -257,15 +308,15 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(2)", + "uri": "record(3)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -281,7 +332,7 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(5)", + "uri": "record(6)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -296,7 +347,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(6)", }, "size": 4114, }, @@ -307,7 +358,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(6)", + "$link": "cids(7)", }, "size": 12736, }, @@ -316,8 +367,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(3)", - "uri": "record(2)", + "cid": "cids(4)", + "uri": "record(3)", }, }, }, @@ -334,8 +385,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(4)", - "uri": "record(5)", + "cid": "cids(5)", + "uri": "record(6)", }, }, "facets": Array [ @@ -356,7 +407,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(9)", + "uri": "record(10)", "viewer": Object {}, }, Object { @@ -365,13 +416,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -380,19 +448,19 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(9)", - "uri": "record(12)", + "cid": "cids(10)", + "uri": "record(13)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(1)", + "cid": "cids(3)", + "uri": "record(2)", }, }, "text": "thanks bob", }, "replyCount": 0, "repostCount": 0, - "uri": "record(11)", + "uri": "record(12)", "viewer": Object {}, }, ] diff --git a/packages/bsky/tests/views/__snapshots__/profile.test.ts.snap b/packages/bsky/tests/views/__snapshots__/profile.test.ts.snap index 8308d94bc6f..62b88f825e0 100644 --- a/packages/bsky/tests/views/__snapshots__/profile.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/profile.test.ts.snap @@ -11,7 +11,24 @@ Array [ "followsCount": 3, "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "postsCount": 4, "viewer": Object { "blockedBy": false, @@ -45,7 +62,7 @@ Array [ "postsCount": 2, "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(3)", "muted": false, }, }, @@ -58,7 +75,7 @@ Array [ "postsCount": 2, "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", + "followedBy": "record(4)", "muted": false, }, }, @@ -75,7 +92,24 @@ Object { "followsCount": 3, "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "postsCount": 4, "viewer": Object { "blockedBy": false, @@ -112,7 +146,24 @@ Object { "followsCount": 3, "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label-b", + }, + ], "postsCount": 4, "viewer": Object { "blockedBy": false, diff --git a/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap b/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap index 7a56218258e..f9aba1e54a0 100644 --- a/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap @@ -13,7 +13,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -21,7 +38,7 @@ Object { "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -32,9 +49,9 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object { - "like": "record(6)", + "like": "record(7)", }, }, "replies": Array [], @@ -51,33 +68,33 @@ Object { "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)@jpeg", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label", }, Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label-2", }, ], @@ -94,7 +111,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -103,19 +120,19 @@ Object { }, "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "replies": Array [], @@ -126,7 +143,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -143,12 +177,12 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(3)", - "uri": "record(4)", + "cid": "cids(4)", + "uri": "record(5)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "text": "thanks bob", @@ -157,7 +191,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "repost": "record(5)", + "repost": "record(6)", }, }, "replies": Array [], @@ -173,7 +207,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -194,7 +245,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", + "like": "record(4)", }, }, "replies": Array [ @@ -207,11 +258,11 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(5)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -232,7 +283,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "replies": Array [], @@ -251,33 +302,33 @@ Object { "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(4)@jpeg", + "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", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "test-label", }, Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "test-label-2", }, ], @@ -294,7 +345,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -315,7 +366,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(6)", + "uri": "record(7)", "viewer": Object {}, }, "replies": Array [ @@ -327,7 +378,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -335,7 +403,7 @@ Object { "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -344,8 +412,8 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(3)", - "uri": "record(6)", + "cid": "cids(4)", + "uri": "record(7)", }, "root": Object { "cid": "cids(0)", @@ -356,9 +424,9 @@ Object { }, "replyCount": 0, "repostCount": 1, - "uri": "record(7)", + "uri": "record(8)", "viewer": Object { - "repost": "record(8)", + "repost": "record(9)", }, }, "replies": Array [], @@ -378,7 +446,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -399,7 +484,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", + "like": "record(4)", }, }, "replies": Array [ @@ -412,11 +497,11 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(5)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -437,7 +522,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, }, @@ -455,33 +540,33 @@ Object { "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(4)@jpeg", + "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", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "test-label", }, Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "test-label-2", }, ], @@ -498,7 +583,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -519,7 +604,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(6)", + "uri": "record(7)", "viewer": Object {}, }, }, @@ -536,7 +621,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -573,7 +675,7 @@ Object { "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -594,7 +696,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object {}, }, "replies": Array [ @@ -606,7 +708,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -614,7 +733,7 @@ Object { "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -623,8 +742,8 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, "root": Object { "cid": "cids(0)", @@ -635,7 +754,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "replies": Array [], @@ -655,7 +774,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -687,7 +823,7 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#notFoundPost", "notFound": true, - "uri": "record(4)", + "uri": "record(5)", }, "post": Object { "author": Object { @@ -695,7 +831,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -712,12 +865,12 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(3)", - "uri": "record(4)", + "cid": "cids(4)", + "uri": "record(5)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "text": "Reply reply", @@ -737,7 +890,7 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#notFoundPost", "notFound": true, - "uri": "record(4)", + "uri": "record(5)", }, "post": Object { "author": Object { @@ -745,7 +898,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -762,12 +932,12 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(3)", - "uri": "record(4)", + "cid": "cids(4)", + "uri": "record(5)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "text": "thanks bob", @@ -776,7 +946,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "repost": "record(5)", + "repost": "record(6)", }, }, "replies": Array [], @@ -789,7 +959,7 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#notFoundPost", "notFound": true, - "uri": "record(4)", + "uri": "record(5)", }, "post": Object { "author": Object { @@ -797,7 +967,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -814,12 +1001,12 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(3)", - "uri": "record(4)", + "cid": "cids(4)", + "uri": "record(5)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "text": "thanks bob", @@ -828,7 +1015,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "repost": "record(5)", + "repost": "record(6)", }, }, "replies": Array [], @@ -844,7 +1031,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -865,7 +1069,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", + "like": "record(4)", }, }, "replies": Array [ @@ -883,33 +1087,33 @@ Object { "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)@jpeg", + "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", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label", }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label-2", }, ], @@ -926,7 +1130,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -947,7 +1151,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "replies": Array [ @@ -959,7 +1163,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -967,7 +1188,7 @@ Object { "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(5)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -976,8 +1197,8 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(4)", + "cid": "cids(3)", + "uri": "record(5)", }, "root": Object { "cid": "cids(0)", @@ -988,9 +1209,9 @@ Object { }, "replyCount": 0, "repostCount": 1, - "uri": "record(5)", + "uri": "record(6)", "viewer": Object { - "repost": "record(6)", + "repost": "record(7)", }, }, "replies": Array [], @@ -1010,7 +1231,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1031,7 +1269,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", + "like": "record(4)", }, }, "replies": Array [ @@ -1049,33 +1287,33 @@ Object { "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)@jpeg", + "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", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label", }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label-2", }, ], @@ -1092,7 +1330,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -1113,7 +1351,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "replies": Array [], diff --git a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap index 17af93d6cea..cee906e2b25 100644 --- a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap @@ -9,7 +9,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -37,7 +54,7 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, @@ -51,13 +68,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -66,8 +100,8 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(3)", - "uri": "record(3)", + "cid": "cids(4)", + "uri": "record(4)", }, "root": Object { "cid": "cids(0)", @@ -78,7 +112,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, }, @@ -89,13 +123,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(5)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -106,31 +157,31 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewNotFound", - "uri": "record(6)", + "uri": "record(7)", }, }, ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(5)", + "uri": "record(6)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(6)", - "uri": "record(6)", + "cid": "cids(7)", + "uri": "record(7)", }, }, "facets": Array [ @@ -154,11 +205,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(4)", + "cid": "cids(5)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label", }, ], @@ -169,15 +220,15 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(5)", - "uri": "record(5)", + "cid": "cids(6)", + "uri": "record(6)", }, }, "text": "yoohoo label_me", }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, }, @@ -188,7 +239,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -217,16 +285,16 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewNotFound", - "uri": "record(6)", + "uri": "record(7)", }, }, "indexedAt": "1970-01-01T00:00:00.000Z", @@ -238,8 +306,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(6)", - "uri": "record(6)", + "cid": "cids(7)", + "uri": "record(7)", }, }, "facets": Array [ @@ -260,7 +328,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(5)", + "uri": "record(6)", "viewer": Object {}, }, }, @@ -272,11 +340,11 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -287,7 +355,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(7)", + "uri": "record(8)", "viewer": Object {}, }, }, @@ -298,24 +366,58 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(9)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(9)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object {}, }, }, @@ -331,7 +433,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -359,7 +478,7 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, @@ -373,13 +492,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -388,8 +524,8 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(3)", - "uri": "record(3)", + "cid": "cids(4)", + "uri": "record(4)", }, "root": Object { "cid": "cids(0)", @@ -400,7 +536,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, }, @@ -412,12 +548,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(7)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(5)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -438,7 +574,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "reply": Object { @@ -449,7 +585,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -476,7 +629,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -505,28 +675,45 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewNotFound", - "uri": "record(8)", + "uri": "record(9)", }, }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(7)", + "uri": "record(8)", "val": "test-label", }, ], @@ -537,15 +724,15 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(7)", + "uri": "record(9)", }, }, "text": "yoohoo label_me", }, "replyCount": 0, "repostCount": 0, - "uri": "record(7)", + "uri": "record(8)", "viewer": Object {}, }, }, @@ -559,12 +746,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", + "followedBy": "record(12)", + "following": "record(11)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -575,7 +762,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(9)", + "uri": "record(10)", "viewer": Object {}, }, }, @@ -586,7 +773,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -615,11 +819,11 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -630,7 +834,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(12)", + "uri": "record(13)", "viewer": Object {}, }, }, @@ -642,12 +846,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(7)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(10)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -655,13 +859,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.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", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(11)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(11)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "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", + "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", }, ], }, @@ -676,25 +880,25 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", + "followedBy": "record(12)", + "following": "record(11)", "muted": false, }, }, - "cid": "cids(12)", + "cid": "cids(13)", "embeds": Array [], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(12)", + "cid": "cids(13)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(14)", + "uri": "record(15)", "val": "kind", }, ], - "uri": "record(14)", + "uri": "record(15)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -710,11 +914,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(9)", + "cid": "cids(10)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(13)", + "uri": "record(14)", "val": "kind", }, ], @@ -733,7 +937,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(10)", + "$link": "cids(11)", }, "size": 4114, }, @@ -744,7 +948,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(11)", + "$link": "cids(12)", }, "size": 12736, }, @@ -753,8 +957,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(12)", - "uri": "record(14)", + "cid": "cids(13)", + "uri": "record(15)", }, }, }, @@ -762,9 +966,9 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(13)", + "uri": "record(14)", "viewer": Object { - "like": "record(15)", + "like": "record(16)", }, }, }, @@ -778,20 +982,20 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", + "followedBy": "record(12)", + "following": "record(11)", "muted": false, }, }, - "cid": "cids(12)", + "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(12)", + "cid": "cids(13)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(14)", + "uri": "record(15)", "val": "kind", }, ], @@ -807,7 +1011,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(14)", + "uri": "record(15)", "viewer": Object {}, }, }, @@ -818,24 +1022,58 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(13)", + "cid": "cids(14)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(14)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(17)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(16)", + "uri": "record(17)", "viewer": Object {}, }, }, @@ -851,7 +1089,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -879,7 +1134,7 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, @@ -894,11 +1149,11 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -909,12 +1164,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -923,13 +1178,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "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", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)@jpeg", }, ], }, @@ -944,24 +1199,24 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "kind", }, ], - "uri": "record(6)", + "uri": "record(7)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -978,15 +1233,15 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(3)", + "uri": "record(4)", "val": "kind", }, ], - "uri": "record(3)", + "uri": "record(4)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1001,7 +1256,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1012,7 +1267,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(6)", }, "size": 12736, }, @@ -1021,8 +1276,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(6)", - "uri": "record(6)", + "cid": "cids(7)", + "uri": "record(7)", }, }, }, @@ -1039,8 +1294,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(3)", - "uri": "record(3)", + "cid": "cids(4)", + "uri": "record(4)", }, }, "facets": Array [ @@ -1061,7 +1316,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, "reason": Object { @@ -1072,8 +1327,8 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, @@ -1087,13 +1342,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1102,8 +1374,8 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(8)", - "uri": "record(10)", + "cid": "cids(9)", + "uri": "record(11)", }, "root": Object { "cid": "cids(0)", @@ -1114,7 +1386,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(9)", + "uri": "record(10)", "viewer": Object {}, }, "reply": Object { @@ -1128,38 +1400,38 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(4)@jpeg", + "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", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(8)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(10)", + "uri": "record(11)", "val": "test-label", }, Object { - "cid": "cids(8)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(10)", + "uri": "record(11)", "val": "test-label-2", }, ], @@ -1176,7 +1448,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1197,7 +1469,7 @@ Array [ }, "replyCount": 1, "repostCount": 0, - "uri": "record(10)", + "uri": "record(11)", "viewer": Object {}, }, "root": Object { @@ -1207,7 +1479,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1237,12 +1526,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1263,7 +1552,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(11)", + "uri": "record(12)", "viewer": Object {}, }, "reply": Object { @@ -1274,7 +1563,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1301,7 +1607,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1333,38 +1656,38 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(4)@jpeg", + "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", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(8)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(10)", + "uri": "record(11)", "val": "test-label", }, Object { - "cid": "cids(8)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(10)", + "uri": "record(11)", "val": "test-label-2", }, ], @@ -1381,7 +1704,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1402,7 +1725,7 @@ Array [ }, "replyCount": 1, "repostCount": 0, - "uri": "record(10)", + "uri": "record(11)", "viewer": Object {}, }, "reply": Object { @@ -1413,7 +1736,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1440,7 +1780,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1469,13 +1826,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(11)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -1486,11 +1860,11 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", @@ -1502,24 +1876,24 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(3)", + "uri": "record(4)", "val": "kind", }, ], - "uri": "record(3)", + "uri": "record(4)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1534,7 +1908,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1545,7 +1919,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(6)", }, "size": 12736, }, @@ -1554,8 +1928,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(6)", - "uri": "record(6)", + "cid": "cids(7)", + "uri": "record(7)", }, }, }, @@ -1566,15 +1940,15 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(2)", + "uri": "record(3)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(3)", - "uri": "record(3)", + "cid": "cids(4)", + "uri": "record(4)", }, }, "facets": Array [ @@ -1598,11 +1972,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(10)", + "cid": "cids(11)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(12)", + "uri": "record(13)", "val": "test-label", }, ], @@ -1613,15 +1987,15 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(2)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(3)", }, }, "text": "yoohoo label_me", }, "replyCount": 0, "repostCount": 0, - "uri": "record(12)", + "uri": "record(13)", "viewer": Object {}, }, }, @@ -1635,12 +2009,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1651,7 +2025,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(13)", + "uri": "record(14)", "viewer": Object {}, }, }, @@ -1662,7 +2036,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1691,11 +2082,11 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -1706,12 +2097,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -1720,13 +2111,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "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", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)@jpeg", }, ], }, @@ -1741,24 +2132,24 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "kind", }, ], - "uri": "record(6)", + "uri": "record(7)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1775,15 +2166,15 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(3)", + "uri": "record(4)", "val": "kind", }, ], - "uri": "record(3)", + "uri": "record(4)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1798,7 +2189,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1809,7 +2200,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(6)", }, "size": 12736, }, @@ -1818,8 +2209,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(6)", - "uri": "record(6)", + "cid": "cids(7)", + "uri": "record(7)", }, }, }, @@ -1836,8 +2227,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(3)", - "uri": "record(3)", + "cid": "cids(4)", + "uri": "record(4)", }, }, "facets": Array [ @@ -1858,7 +2249,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, }, @@ -1870,11 +2261,11 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(12)", + "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1885,7 +2276,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(14)", + "uri": "record(15)", "viewer": Object {}, }, }, @@ -1897,12 +2288,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -1910,13 +2301,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "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", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)@jpeg", }, ], }, @@ -1931,25 +2322,25 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "embeds": Array [], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "kind", }, ], - "uri": "record(6)", + "uri": "record(7)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1965,11 +2356,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(3)", + "uri": "record(4)", "val": "kind", }, ], @@ -1988,7 +2379,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1999,7 +2390,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(6)", }, "size": 12736, }, @@ -2008,8 +2399,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(6)", - "uri": "record(6)", + "cid": "cids(7)", + "uri": "record(7)", }, }, }, @@ -2017,9 +2408,9 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object { - "like": "record(15)", + "like": "record(16)", }, }, }, @@ -2033,20 +2424,20 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "kind", }, ], @@ -2062,7 +2453,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(6)", + "uri": "record(7)", "viewer": Object {}, }, }, @@ -2073,24 +2464,58 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(13)", + "cid": "cids(14)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(14)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(17)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(16)", + "uri": "record(17)", "viewer": Object {}, }, }, @@ -2296,7 +2721,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(7)", @@ -2313,12 +2755,12 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(8)", - "uri": "record(9)", + "cid": "cids(9)", + "uri": "record(10)", }, "root": Object { - "cid": "cids(7)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(9)", }, }, "text": "thanks bob", @@ -2342,7 +2784,7 @@ Array [ "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -2356,19 +2798,19 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(8)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(10)", "val": "test-label", }, Object { - "cid": "cids(8)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(10)", "val": "test-label-2", }, ], @@ -2394,19 +2836,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(7)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(9)", }, "root": Object { - "cid": "cids(7)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(9)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(9)", + "uri": "record(10)", "viewer": Object {}, }, "root": Object { @@ -2416,7 +2858,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(7)", @@ -2424,7 +2883,7 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -2435,9 +2894,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object { - "like": "record(10)", + "like": "record(11)", }, }, }, @@ -2454,7 +2913,7 @@ Array [ "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -2463,19 +2922,19 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(7)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(9)", }, "root": Object { - "cid": "cids(7)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(9)", }, }, "text": "of course", }, "replyCount": 0, "repostCount": 0, - "uri": "record(11)", + "uri": "record(12)", "viewer": Object {}, }, "reply": Object { @@ -2486,7 +2945,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(7)", @@ -2494,7 +2970,7 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -2505,9 +2981,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object { - "like": "record(10)", + "like": "record(11)", }, }, "root": Object { @@ -2517,7 +2993,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(7)", @@ -2525,7 +3018,7 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -2536,9 +3029,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object { - "like": "record(10)", + "like": "record(11)", }, }, }, @@ -2556,7 +3049,7 @@ Array [ "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -2570,19 +3063,19 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(8)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(10)", "val": "test-label", }, Object { - "cid": "cids(8)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(10)", "val": "test-label-2", }, ], @@ -2608,19 +3101,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(7)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(9)", }, "root": Object { - "cid": "cids(7)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(9)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(9)", + "uri": "record(10)", "viewer": Object {}, }, "reply": Object { @@ -2631,7 +3124,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(7)", @@ -2639,7 +3149,7 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -2650,9 +3160,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object { - "like": "record(10)", + "like": "record(11)", }, }, "root": Object { @@ -2662,7 +3172,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(7)", @@ -2670,7 +3197,7 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -2681,9 +3208,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object { - "like": "record(10)", + "like": "record(11)", }, }, }, @@ -2695,7 +3222,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(7)", @@ -2703,7 +3247,7 @@ Array [ "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(11)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -2825,11 +3369,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(10)", + "cid": "cids(11)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(12)", + "uri": "record(13)", "val": "test-label", }, ], @@ -2848,9 +3392,9 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(12)", + "uri": "record(13)", "viewer": Object { - "like": "record(13)", + "like": "record(14)", }, }, }, @@ -2867,7 +3411,7 @@ Array [ "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -2878,7 +3422,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(14)", + "uri": "record(15)", "viewer": Object {}, }, }, @@ -2889,7 +3433,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(7)", @@ -2897,7 +3458,7 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -2908,9 +3469,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object { - "like": "record(10)", + "like": "record(11)", }, }, }, @@ -3041,7 +3602,7 @@ Array [ "repostCount": 0, "uri": "record(2)", "viewer": Object { - "like": "record(15)", + "like": "record(16)", }, }, }, @@ -3093,7 +3654,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(7)", @@ -3101,18 +3679,35 @@ Array [ "muted": false, }, }, - "cid": "cids(12)", + "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(13)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(17)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(16)", + "uri": "record(17)", "viewer": Object {}, }, }, @@ -3318,7 +3913,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(7)", @@ -3335,12 +3947,12 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(8)", - "uri": "record(9)", + "cid": "cids(9)", + "uri": "record(10)", }, "root": Object { - "cid": "cids(7)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(9)", }, }, "text": "thanks bob", @@ -3365,7 +3977,7 @@ Array [ "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -3379,19 +3991,19 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(8)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(10)", "val": "test-label", }, Object { - "cid": "cids(8)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(10)", "val": "test-label-2", }, ], @@ -3417,19 +4029,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(7)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(9)", }, "root": Object { - "cid": "cids(7)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(9)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(9)", + "uri": "record(10)", "viewer": Object {}, }, "root": Object { @@ -3439,7 +4051,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(7)", @@ -3447,7 +4076,7 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -3458,9 +4087,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object { - "like": "record(10)", + "like": "record(11)", }, }, }, @@ -3476,7 +4105,7 @@ Array [ "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -3485,19 +4114,19 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(7)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(9)", }, "root": Object { - "cid": "cids(7)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(9)", }, }, "text": "of course", }, "replyCount": 0, "repostCount": 0, - "uri": "record(11)", + "uri": "record(12)", "viewer": Object {}, }, "reply": Object { @@ -3508,7 +4137,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(7)", @@ -3516,7 +4162,7 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -3527,9 +4173,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object { - "like": "record(10)", + "like": "record(11)", }, }, "root": Object { @@ -3539,7 +4185,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(7)", @@ -3547,7 +4210,7 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -3558,9 +4221,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object { - "like": "record(10)", + "like": "record(11)", }, }, }, @@ -3572,7 +4235,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(7)", @@ -3580,7 +4260,7 @@ Array [ "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(11)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -3700,11 +4380,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(10)", + "cid": "cids(11)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(12)", + "uri": "record(13)", "val": "test-label", }, ], @@ -3723,9 +4403,9 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(12)", + "uri": "record(13)", "viewer": Object { - "like": "record(13)", + "like": "record(14)", }, }, }, @@ -3736,7 +4416,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(7)", @@ -3744,7 +4441,7 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -3755,9 +4452,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object { - "like": "record(10)", + "like": "record(11)", }, }, }, @@ -3897,7 +4594,24 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(7)", @@ -3905,18 +4619,35 @@ Array [ "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(12)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(15)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(14)", + "uri": "record(15)", "viewer": Object {}, }, }, @@ -3932,7 +4663,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -3952,8 +4700,8 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", - "repost": "record(2)", + "like": "record(4)", + "repost": "record(3)", }, }, "reason": Object { @@ -3980,37 +4728,37 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(5)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(3)@jpeg", + "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", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label", }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label-2", }, ], @@ -4027,7 +4775,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -4048,7 +4796,7 @@ Array [ }, "replyCount": 1, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "reply": Object { @@ -4059,7 +4807,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -4079,8 +4844,8 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", - "repost": "record(2)", + "like": "record(4)", + "repost": "record(3)", }, }, "root": Object { @@ -4090,7 +4855,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -4110,8 +4892,8 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", - "repost": "record(2)", + "like": "record(4)", + "repost": "record(3)", }, }, }, @@ -4126,11 +4908,11 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(5)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(5)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -4141,7 +4923,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(6)", + "uri": "record(7)", "viewer": Object {}, }, }, @@ -4156,7 +4938,7 @@ Array [ "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -4170,7 +4952,7 @@ Array [ "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -4179,13 +4961,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.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", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(4)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(4)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(7)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(7)@jpeg", + "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", }, ], }, @@ -4200,23 +4982,23 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(5)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(8)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(10)", "val": "kind", }, ], - "uri": "record(9)", + "uri": "record(10)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -4233,15 +5015,15 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(8)", + "uri": "record(9)", "val": "kind", }, ], - "uri": "record(8)", + "uri": "record(9)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -4256,7 +5038,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -4267,7 +5049,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(7)", + "$link": "cids(8)", }, "size": 12736, }, @@ -4276,8 +5058,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(8)", - "uri": "record(9)", + "cid": "cids(9)", + "uri": "record(10)", }, }, }, @@ -4294,8 +5076,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(7)", + "uri": "record(9)", }, }, "facets": Array [ @@ -4316,7 +5098,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(7)", + "uri": "record(8)", "viewer": Object {}, }, }, @@ -4331,7 +5113,7 @@ Array [ "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -4342,7 +5124,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(10)", + "uri": "record(11)", "viewer": Object {}, }, }, @@ -4356,19 +5138,19 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(5)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(8)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(10)", "val": "kind", }, ], @@ -4384,7 +5166,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(9)", + "uri": "record(10)", "viewer": Object {}, }, }, diff --git a/packages/bsky/tests/views/profile.test.ts b/packages/bsky/tests/views/profile.test.ts index ecdbf983e6b..a1224283794 100644 --- a/packages/bsky/tests/views/profile.test.ts +++ b/packages/bsky/tests/views/profile.test.ts @@ -48,6 +48,20 @@ describe('pds profile views', () => { expect(forSnapshot(aliceForAlice.data)).toMatchSnapshot() }) + it('reflects self-labels', async () => { + const aliceForBob = await agent.api.app.bsky.actor.getProfile( + { actor: alice }, + { headers: await network.serviceHeaders(bob) }, + ) + + const labels = aliceForBob.data.labels + ?.filter((label) => label.src === alice) + .map((label) => label.val) + .sort() + + expect(labels).toEqual(['self-label-a', 'self-label-b']) + }) + it("fetches other's profile, with a follow", async () => { const aliceForBob = await agent.api.app.bsky.actor.getProfile( { actor: alice }, diff --git a/packages/bsky/tests/views/thread.test.ts b/packages/bsky/tests/views/thread.test.ts index 7df31e24b87..d1c96f38603 100644 --- a/packages/bsky/tests/views/thread.test.ts +++ b/packages/bsky/tests/views/thread.test.ts @@ -4,6 +4,8 @@ import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { forSnapshot, stripViewerFromThread } from '../_util' import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' +import assert from 'assert' +import { isThreadViewPost } from '@atproto/api/src/client/types/app/bsky/feed/defs' describe('pds thread views', () => { let network: TestNetwork @@ -141,6 +143,29 @@ describe('pds thread views', () => { expect(forSnapshot(thread3.data.thread)).toMatchSnapshot() }) + it('reflects self-labels', async () => { + const { data: thread } = await agent.api.app.bsky.feed.getPostThread( + { uri: sc.posts[alice][0].ref.uriStr }, + { headers: await network.serviceHeaders(bob) }, + ) + + assert(isThreadViewPost(thread.thread), 'post does not exist') + const post = thread.thread.post + + const postSelfLabels = post.labels + ?.filter((label) => label.src === alice) + .map((label) => label.val) + + expect(postSelfLabels).toEqual(['self-label']) + + const authorSelfLabels = post.author.labels + ?.filter((label) => label.src === alice) + .map((label) => label.val) + .sort() + + expect(authorSelfLabels).toEqual(['self-label-a', 'self-label-b']) + }) + describe('takedown', () => { it('blocks post by actor', async () => { const { data: modAction } = diff --git a/packages/bsky/tests/views/timeline.test.ts b/packages/bsky/tests/views/timeline.test.ts index c1cf35ce8fa..e3111ebc5b8 100644 --- a/packages/bsky/tests/views/timeline.test.ts +++ b/packages/bsky/tests/views/timeline.test.ts @@ -1,3 +1,4 @@ +import assert from 'assert' import AtpAgent from '@atproto/api' import { TestNetwork } from '@atproto/dev-env' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' @@ -156,6 +157,32 @@ describe('timeline views', () => { expect(results(paginatedAll)).toEqual(results([full.data])) }) + it('reflects self-labels', async () => { + const carolTL = await agent.api.app.bsky.feed.getTimeline( + {}, + { headers: await network.serviceHeaders(carol) }, + ) + + const alicePost = carolTL.data.feed.find( + ({ post }) => post.uri === sc.posts[alice][0].ref.uriStr, + )?.post + + assert(alicePost, 'post does not exist') + + const postSelfLabels = alicePost.labels + ?.filter((label) => label.src === alice) + .map((label) => label.val) + + expect(postSelfLabels).toEqual(['self-label']) + + const authorSelfLabels = alicePost.author.labels + ?.filter((label) => label.src === alice) + .map((label) => label.val) + .sort() + + expect(authorSelfLabels).toEqual(['self-label-a', 'self-label-b']) + }) + it('blocks posts, reposts, replies by actor takedown', async () => { const actionResults = await Promise.all( [bob, carol].map((did) => diff --git a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts b/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts index e97f54992ba..0e17234f7ab 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts +++ b/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts @@ -5,6 +5,7 @@ import { Server } from '../../../../../lexicon' import { paginate, TimeCidKeyset } from '../../../../../db/pagination' import AppContext from '../../../../../context' import { notSoftDeletedClause, valuesList } from '../../../../../db/util' +import { getSelfLabels } from '../../../../services/label' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.listNotifications({ @@ -131,16 +132,23 @@ export default function (server: Server, ctx: AppContext) { const bytes = bytesByCid[notif.cid] const author = authors[notif.authorDid] if (!bytes || !author) return undefined + const record = common.cborBytesToRecord(bytes) + const recordLabels = labels[notif.uri] ?? [] + const recordSelfLabels = getSelfLabels({ + uri: notif.uri, + cid: notif.cid, + record, + }) return { uri: notif.uri, cid: notif.cid, author: authors[notif.authorDid], reason: notif.reason, reasonSubject: notif.reasonSubject || undefined, - record: common.cborBytesToRecord(bytes), + record, isRead: notif.indexedAt <= userState.lastSeenNotifs, indexedAt: notif.indexedAt, - labels: labels[notif.uri] ?? [], + labels: [...recordLabels, ...recordSelfLabels], } }) diff --git a/packages/pds/src/app-view/services/actor/views.ts b/packages/pds/src/app-view/services/actor/views.ts index 6f0e6a8d23b..1d5468cedd2 100644 --- a/packages/pds/src/app-view/services/actor/views.ts +++ b/packages/pds/src/app-view/services/actor/views.ts @@ -1,4 +1,5 @@ import { mapDefined } from '@atproto/common' +import { cborToLexRecord } from '@atproto/repo' import { ProfileViewDetailed, ProfileView, @@ -7,7 +8,7 @@ import { import { DidHandle } from '../../../db/tables/did-handle' import Database from '../../../db' import { ImageUriBuilder } from '../../../image/uri' -import { LabelService } from '../label' +import { LabelService, getSelfLabels } from '../label' import { GraphService } from '../graph' import { LabelCache } from '../../../label-cache' import { notSoftDeletedClause } from '../../../db/util' @@ -42,6 +43,11 @@ export class ActorViews { .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') .leftJoin('profile', 'profile.creator', 'did_handle.did') .leftJoin('profile_agg', 'profile_agg.did', 'did_handle.did') + .leftJoin('ipld_block', (join) => + join + .onRef('ipld_block.cid', '=', 'profile.cid') + .onRef('ipld_block.creator', '=', 'profile.creator'), + ) .if(!includeSoftDeleted, (qb) => qb.where(notSoftDeletedClause(ref('repo_root'))), ) @@ -49,6 +55,7 @@ export class ActorViews { 'did_handle.did as did', 'did_handle.handle as handle', 'profile.uri as profileUri', + 'profile.cid as profileCid', 'profile.displayName as displayName', 'profile.description as description', 'profile.avatarCid as avatarCid', @@ -57,6 +64,7 @@ export class ActorViews { 'profile_agg.followsCount as followsCount', 'profile_agg.followersCount as followersCount', 'profile_agg.postsCount as postsCount', + 'ipld_block.content as profileBytes', this.db.db .selectFrom('follow') .where('creator', '=', viewer) @@ -108,7 +116,6 @@ export class ActorViews { const listViews = await this.services.graph.getListViews(listUris, viewer) return profileInfos.reduce((acc, cur) => { - const actorLabels = labels[cur.did] ?? [] const avatar = cur?.avatarCid ? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid) : undefined @@ -121,6 +128,12 @@ export class ActorViews { listViews[cur.requesterMutedByList], ) : undefined + const actorLabels = labels[cur.did] ?? [] + const selfLabels = getSelfLabels({ + uri: cur.profileUri, + cid: cur.profileCid, + record: cur.profileBytes && cborToLexRecord(cur.profileBytes), + }) const profile = { did: cur.did, handle: cur.handle, @@ -140,7 +153,7 @@ export class ActorViews { following: cur?.requesterFollowing || undefined, followedBy: cur?.requesterFollowedBy || undefined, }, - labels: skipLabels ? undefined : actorLabels, + labels: skipLabels ? undefined : [...actorLabels, ...selfLabels], } acc[cur.did] = profile return acc @@ -181,6 +194,11 @@ export class ActorViews { .where('did_handle.did', 'in', dids) .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') .leftJoin('profile', 'profile.creator', 'did_handle.did') + .leftJoin('ipld_block', (join) => + join + .onRef('ipld_block.cid', '=', 'profile.cid') + .onRef('ipld_block.creator', '=', 'profile.creator'), + ) .if(!includeSoftDeleted, (qb) => qb.where(notSoftDeletedClause(ref('repo_root'))), ) @@ -188,10 +206,12 @@ export class ActorViews { 'did_handle.did as did', 'did_handle.handle as handle', 'profile.uri as profileUri', + 'profile.cid as profileCid', 'profile.displayName as displayName', 'profile.description as description', 'profile.avatarCid as avatarCid', 'profile.indexedAt as indexedAt', + 'ipld_block.content as profileBytes', this.db.db .selectFrom('follow') .where('creator', '=', viewer) @@ -243,7 +263,6 @@ export class ActorViews { const listViews = await this.services.graph.getListViews(listUris, viewer) return profileInfos.reduce((acc, cur) => { - const actorLabels = labels[cur.did] ?? [] const avatar = cur.avatarCid ? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid) : undefined @@ -253,6 +272,12 @@ export class ActorViews { listViews[cur.requesterMutedByList], ) : undefined + const actorLabels = labels[cur.did] ?? [] + const selfLabels = getSelfLabels({ + uri: cur.profileUri, + cid: cur.profileCid, + record: cur.profileBytes && cborToLexRecord(cur.profileBytes), + }) const profile = { did: cur.did, handle: cur.handle, @@ -268,7 +293,7 @@ export class ActorViews { following: cur?.requesterFollowing || undefined, followedBy: cur?.requesterFollowedBy || undefined, }, - labels: skipLabels ? undefined : actorLabels, + labels: skipLabels ? undefined : [...actorLabels, ...selfLabels], } acc[cur.did] = profile return acc diff --git a/packages/pds/src/app-view/services/feed/index.ts b/packages/pds/src/app-view/services/feed/index.ts index d26f43d3702..f835d7cac51 100644 --- a/packages/pds/src/app-view/services/feed/index.ts +++ b/packages/pds/src/app-view/services/feed/index.ts @@ -30,8 +30,9 @@ import { PostBlocksMap, RecordEmbedViewRecord, FeedHydrationOptions, + kSelfLabels, } from './types' -import { LabelService, Labels } from '../label' +import { LabelService, Labels, getSelfLabels } from '../label' import { ActorService } from '../actor' import { GraphService } from '../graph' import { FeedViews } from './views' @@ -131,16 +132,23 @@ export class FeedService { .where('did_handle.did', 'in', dids) .innerJoin('repo_root', 'repo_root.did', 'did_handle.did') .leftJoin('profile', 'profile.creator', 'did_handle.did') + .leftJoin('ipld_block', (join) => + join + .onRef('ipld_block.cid', '=', 'profile.cid') + .onRef('ipld_block.creator', '=', 'profile.creator'), + ) .selectAll('did_handle') .if(!includeSoftDeleted, (qb) => qb.where(notSoftDeletedClause(ref('repo_root'))), ) .select([ 'profile.uri as profileUri', + 'profile.cid as profileCid', 'profile.displayName as displayName', 'profile.description as description', 'profile.avatarCid as avatarCid', 'profile.indexedAt as indexedAt', + 'ipld_block.content as profileBytes', this.db.db .selectFrom('follow') .where('creator', '=', requester ?? '') @@ -191,7 +199,6 @@ export class FeedService { requester, ) return actors.reduce((acc, cur) => { - const actorLabels = labels[cur.did] ?? [] const avatar = cur.avatarCid ? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid) : undefined @@ -201,6 +208,12 @@ export class FeedService { listViews[cur.requesterMutedByList], ) : undefined + const actorLabels = labels[cur.did] ?? [] + const selfLabels = getSelfLabels({ + uri: cur.profileUri, + cid: cur.profileCid, + record: cur.profileBytes && cborToLexRecord(cur.profileBytes), + }) return { ...acc, [cur.did]: { @@ -216,7 +229,8 @@ export class FeedService { following: cur?.requesterFollowing || undefined, followedBy: cur?.requesterFollowedBy || undefined, }, - labels: skipLabels ? undefined : actorLabels, + labels: skipLabels ? undefined : [...actorLabels, ...selfLabels], + [kSelfLabels]: selfLabels, }, } }, {} as ActorInfoMap) diff --git a/packages/pds/src/app-view/services/feed/types.ts b/packages/pds/src/app-view/services/feed/types.ts index c0eaea16eef..998caa6e32c 100644 --- a/packages/pds/src/app-view/services/feed/types.ts +++ b/packages/pds/src/app-view/services/feed/types.ts @@ -50,6 +50,8 @@ export type PostBlocksMap = { [uri: string]: { reply?: boolean; embed?: boolean } } +export const kSelfLabels = Symbol('selfLabels') + export type ActorInfo = { did: string handle: string @@ -63,6 +65,8 @@ export type ActorInfo = { followedBy?: string } labels?: Label[] + // allows threading self-labels through if they are going to be applied later, i.e. when using skipLabels option. + [kSelfLabels]?: Label[] } export type ActorInfoMap = { [did: string]: ActorInfo } diff --git a/packages/pds/src/app-view/services/feed/views.ts b/packages/pds/src/app-view/services/feed/views.ts index 27d25675598..846fa928a19 100644 --- a/packages/pds/src/app-view/services/feed/views.ts +++ b/packages/pds/src/app-view/services/feed/views.ts @@ -30,9 +30,9 @@ import { RecordEmbedViewRecord, PostBlocksMap, FeedHydrationOptions, + kSelfLabels, } from './types' -import { Labels } from '../label' -import { ProfileView } from '../../../lexicon/types/app/bsky/actor/defs' +import { Labels, getSelfLabels } from '../label' import { ImageUriBuilder } from '../../../image/uri' export * from './types' @@ -46,14 +46,16 @@ export class FeedViews { formatFeedGeneratorView( info: FeedGenInfo, - profiles: Record, + profiles: ActorInfoMap, labels?: Labels, ): GeneratorView { const profile = profiles[info.creator] - if (profile) { + if (profile && !profile.labels) { // If the creator labels are not hydrated yet, attempt to pull them // from labels: e.g. compatible with embedsForPosts() batching label hydration. - profile.labels ??= labels?.[info.creator] ?? [] + const profileLabels = labels?.[info.creator] ?? [] + const profileSelfLabels = profile[kSelfLabels] ?? [] + profile.labels = [...profileLabels, ...profileSelfLabels] } return { uri: info.uri, @@ -106,11 +108,13 @@ export class FeedViews { if (!originator) { continue } else { + const originatorLabels = labels[item.originatorDid] ?? [] + const originatorSelfLabels = originator[kSelfLabels] ?? [] feedPost['reason'] = { $type: 'app.bsky.feed.defs#reasonRepost', by: { ...originator, - labels: labels[item.originatorDid] ?? [], + labels: [...originatorLabels, ...originatorSelfLabels], }, indexedAt: item.sortAt, } @@ -160,7 +164,15 @@ export class FeedViews { if (!post || !author) return undefined // If the author labels are not hydrated yet, attempt to pull them // from labels: e.g. compatible with hydrateFeed() batching label hydration. - author.labels ??= labels[author.did] ?? [] + const authorLabels = labels[author.did] ?? [] + const authorSelfLabels = author[kSelfLabels] ?? [] + author.labels ??= [...authorLabels, ...authorSelfLabels] + const postLabels = labels[uri] ?? [] + const postSelfLabels = getSelfLabels({ + uri: post.uri, + cid: post.cid, + record: post.record, + }) return { uri: post.uri, cid: post.cid, @@ -178,7 +190,7 @@ export class FeedViews { repost: post.requesterRepost ?? undefined, like: post.requesterLike ?? undefined, }, - labels: labels[uri] ?? [], + labels: [...postLabels, ...postSelfLabels], } } diff --git a/packages/pds/src/app-view/services/label/index.ts b/packages/pds/src/app-view/services/label/index.ts index 1975b5e07db..708384fd059 100644 --- a/packages/pds/src/app-view/services/label/index.ts +++ b/packages/pds/src/app-view/services/label/index.ts @@ -1,9 +1,14 @@ +import { sql } from 'kysely' +import { CID } from 'multiformats/cid' import { AtUri } from '@atproto/uri' import Database from '../../../db' -import { Label } from '../../../lexicon/types/com/atproto/label/defs' +import { + Label, + isSelfLabels, +} from '../../../lexicon/types/com/atproto/label/defs' import { ids } from '../../../lexicon/lexicons' -import { sql } from 'kysely' import { LabelCache } from '../../../label-cache' +import { toSimplifiedISOSafe } from '../indexing/util' export type Labels = Record @@ -147,3 +152,21 @@ export class LabelService { return labels[did] ?? [] } } + +export function getSelfLabels(details: { + uri: string | null + cid: string | null + record: Record | null +}): Label[] { + const { uri, cid, record } = details + if (!uri || !cid || !record) return [] + if (!isSelfLabels(record.labels)) return [] + const src = new AtUri(uri).host // record creator + const cts = + typeof record.createdAt === 'string' + ? toSimplifiedISOSafe(record.createdAt) + : new Date(0).toISOString() + return record.labels.values.map(({ val }) => { + return { src, uri, cid, val, cts, neg: false } + }) +} diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index ed5d0a97f81..780238d5a42 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -1441,6 +1441,36 @@ export const schemaDict = { }, }, }, + 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 -- 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: { @@ -3925,6 +3955,10 @@ export const schemaDict = { accept: ['image/png', 'image/jpeg'], maxSize: 1000000, }, + labels: { + type: 'union', + refs: ['lex:com.atproto.label.defs#selfLabels'], + }, }, }, }, @@ -4671,6 +4705,10 @@ export const schemaDict = { accept: ['image/png', 'image/jpeg'], maxSize: 1000000, }, + labels: { + type: 'union', + refs: ['lex:com.atproto.label.defs#selfLabels'], + }, createdAt: { type: 'string', format: 'datetime', @@ -5330,6 +5368,10 @@ export const schemaDict = { format: 'language', }, }, + labels: { + type: 'union', + refs: ['lex:com.atproto.label.defs#selfLabels'], + }, createdAt: { type: 'string', format: 'datetime', @@ -5949,6 +5991,10 @@ export const schemaDict = { accept: ['image/png', 'image/jpeg'], maxSize: 1000000, }, + labels: { + type: 'union', + refs: ['lex:com.atproto.label.defs#selfLabels'], + }, createdAt: { type: 'string', format: 'datetime', diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/profile.ts b/packages/pds/src/lexicon/types/app/bsky/actor/profile.ts index 2d4bf526601..7dbc4c1ccec 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/profile.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/profile.ts @@ -5,12 +5,16 @@ 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 } diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/generator.ts b/packages/pds/src/lexicon/types/app/bsky/feed/generator.ts index 5ad34318ee3..757e74db845 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/generator.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/generator.ts @@ -6,6 +6,7 @@ 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 @@ -13,6 +14,9 @@ export interface Record { description?: string descriptionFacets?: AppBskyRichtextFacet.Main[] avatar?: BlobRef + labels?: + | ComAtprotoLabelDefs.SelfLabels + | { $type: string; [k: string]: unknown } createdAt: string [k: string]: unknown } diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/post.ts b/packages/pds/src/lexicon/types/app/bsky/feed/post.ts index b111c4783e6..8942bc724cd 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/post.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/post.ts @@ -10,6 +10,7 @@ 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 { @@ -25,6 +26,9 @@ export interface Record { | AppBskyEmbedRecordWithMedia.Main | { $type: string; [k: string]: unknown } langs?: string[] + labels?: + | ComAtprotoLabelDefs.SelfLabels + | { $type: string; [k: string]: unknown } createdAt: string [k: string]: unknown } diff --git a/packages/pds/src/lexicon/types/app/bsky/graph/list.ts b/packages/pds/src/lexicon/types/app/bsky/graph/list.ts index 4304ca98b03..36a7fb17a3f 100644 --- a/packages/pds/src/lexicon/types/app/bsky/graph/list.ts +++ b/packages/pds/src/lexicon/types/app/bsky/graph/list.ts @@ -7,6 +7,7 @@ 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 @@ -14,6 +15,9 @@ export interface Record { description?: string descriptionFacets?: AppBskyRichtextFacet.Main[] avatar?: BlobRef + labels?: + | ComAtprotoLabelDefs.SelfLabels + | { $type: string; [k: string]: unknown } createdAt: string [k: string]: unknown } diff --git a/packages/pds/src/lexicon/types/com/atproto/label/defs.ts b/packages/pds/src/lexicon/types/com/atproto/label/defs.ts index 17a8480b9d6..a01ad78e254 100644 --- a/packages/pds/src/lexicon/types/com/atproto/label/defs.ts +++ b/packages/pds/src/lexicon/types/com/atproto/label/defs.ts @@ -34,3 +34,40 @@ export function isLabel(v: unknown): v is 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 -- 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/pds/tests/__snapshots__/feed-generation.test.ts.snap b/packages/pds/tests/__snapshots__/feed-generation.test.ts.snap index fc302af8cf2..93bc44a6f17 100644 --- a/packages/pds/tests/__snapshots__/feed-generation.test.ts.snap +++ b/packages/pds/tests/__snapshots__/feed-generation.test.ts.snap @@ -7,7 +7,24 @@ Object { "did": "user(0)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -18,7 +35,7 @@ Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewNotFound", - "uri": "record(1)", + "uri": "record(2)", }, }, "indexedAt": "1970-01-01T00:00:00.000Z", @@ -30,8 +47,8 @@ Object { "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(1)", - "uri": "record(1)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "weird feed", @@ -50,7 +67,24 @@ Object { "did": "user(0)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -61,17 +95,34 @@ Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.feed.defs#generatorView", - "cid": "cids(1)", + "cid": "cids(2)", "creator": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", "did": "user(2)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, @@ -80,9 +131,9 @@ Object { "displayName": "All", "indexedAt": "1970-01-01T00:00:00.000Z", "likeCount": 2, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object { - "like": "record(4)", + "like": "record(6)", }, }, }, @@ -95,8 +146,8 @@ Object { "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(1)", - "uri": "record(1)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "cool feed!", @@ -119,7 +170,24 @@ Array [ "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -136,7 +204,7 @@ Array [ "viewer": Object {}, }, Object { - "cid": "cids(1)", + "cid": "cids(2)", "creator": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", "description": "its me!", @@ -144,7 +212,24 @@ Array [ "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -157,11 +242,11 @@ Array [ "displayName": "Bad Pagination", "indexedAt": "1970-01-01T00:00:00.000Z", "likeCount": 0, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object {}, }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "creator": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", "description": "its me!", @@ -169,7 +254,24 @@ Array [ "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -182,11 +284,11 @@ Array [ "displayName": "Even", "indexedAt": "1970-01-01T00:00:00.000Z", "likeCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, Object { - "cid": "cids(3)", + "cid": "cids(4)", "creator": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", "description": "its me!", @@ -194,7 +296,24 @@ Array [ "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -207,9 +326,9 @@ Array [ "displayName": "All", "indexedAt": "1970-01-01T00:00:00.000Z", "likeCount": 2, - "uri": "record(5)", + "uri": "record(6)", "viewer": Object { - "like": "record(6)", + "like": "record(7)", }, }, ] @@ -224,7 +343,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -232,11 +368,28 @@ Array [ }, "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, @@ -252,15 +405,32 @@ Array [ "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -275,7 +445,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -287,12 +457,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(4)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -318,19 +488,36 @@ Array [ "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "embeds": Array [], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(1)", + "uri": "record(2)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -360,7 +547,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -371,7 +558,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(6)", }, "size": 12736, }, @@ -380,8 +567,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(1)", - "uri": "record(1)", + "cid": "cids(2)", + "uri": "record(2)", }, }, }, @@ -389,9 +576,9 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(6)", "viewer": Object { - "like": "record(7)", + "like": "record(9)", }, }, }, @@ -403,12 +590,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -417,19 +604,19 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(6)", - "uri": "record(9)", + "cid": "cids(8)", + "uri": "record(11)", }, "root": Object { - "cid": "cids(6)", - "uri": "record(9)", + "cid": "cids(8)", + "uri": "record(11)", }, }, "text": "of course", }, "replyCount": 0, "repostCount": 0, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object {}, }, "reply": Object { @@ -440,13 +627,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -457,7 +661,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(9)", + "uri": "record(11)", "viewer": Object {}, }, "root": Object { @@ -467,13 +671,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -484,7 +705,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(9)", + "uri": "record(11)", "viewer": Object {}, }, }, @@ -505,11 +726,11 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(11)", + "following": "record(13)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -520,12 +741,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(4)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -552,18 +773,35 @@ Array [ "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(1)", + "uri": "record(2)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -579,7 +817,7 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(4)", + "uri": "record(6)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -594,7 +832,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -605,7 +843,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(6)", }, "size": 12736, }, @@ -614,8 +852,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(1)", - "uri": "record(1)", + "cid": "cids(2)", + "uri": "record(2)", }, }, }, @@ -632,8 +870,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(2)", - "uri": "record(4)", + "cid": "cids(4)", + "uri": "record(6)", }, }, "facets": Array [ @@ -654,7 +892,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(10)", + "uri": "record(12)", "viewer": Object {}, }, "reason": Object { @@ -665,8 +903,8 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, @@ -685,7 +923,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -693,11 +948,28 @@ Array [ }, "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, @@ -714,12 +986,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -745,19 +1017,36 @@ Array [ "did": "user(2)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(7)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(5)", "embeds": Array [], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(4)", + "uri": "record(5)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -787,7 +1076,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(2)", + "$link": "cids(3)", }, "size": 4114, }, @@ -798,7 +1087,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 12736, }, @@ -807,8 +1096,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(4)", - "uri": "record(4)", + "cid": "cids(5)", + "uri": "record(5)", }, }, }, @@ -816,9 +1105,9 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object { - "like": "record(7)", + "like": "record(9)", }, }, }, @@ -830,12 +1119,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -844,19 +1133,19 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(6)", - "uri": "record(9)", + "cid": "cids(8)", + "uri": "record(11)", }, "root": Object { - "cid": "cids(6)", - "uri": "record(9)", + "cid": "cids(8)", + "uri": "record(11)", }, }, "text": "of course", }, "replyCount": 0, "repostCount": 0, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object {}, }, "reply": Object { @@ -867,13 +1156,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -884,7 +1190,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(9)", + "uri": "record(11)", "viewer": Object {}, }, "root": Object { @@ -894,13 +1200,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -911,7 +1234,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(9)", + "uri": "record(11)", "viewer": Object {}, }, }, @@ -930,7 +1253,24 @@ Object { "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -945,7 +1285,7 @@ Object { "likeCount": 2, "uri": "record(0)", "viewer": Object { - "like": "record(3)", + "like": "record(4)", }, }, } @@ -961,7 +1301,24 @@ Object { "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -976,17 +1333,34 @@ Object { "likeCount": 2, "uri": "record(0)", "viewer": Object { - "like": "record(3)", + "like": "record(4)", }, }, Object { - "cid": "cids(1)", + "cid": "cids(2)", "creator": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -999,7 +1373,7 @@ Object { "displayName": "Even", "indexedAt": "1970-01-01T00:00:00.000Z", "likeCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, ], diff --git a/packages/pds/tests/__snapshots__/indexing.test.ts.snap b/packages/pds/tests/__snapshots__/indexing.test.ts.snap index b1810d28696..c4bc3f83cf0 100644 --- a/packages/pds/tests/__snapshots__/indexing.test.ts.snap +++ b/packages/pds/tests/__snapshots__/indexing.test.ts.snap @@ -10,7 +10,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, diff --git a/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap index 46a88622b0f..02a0a420403 100644 --- a/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap @@ -166,6 +166,17 @@ Object { }, "description": "hi im bob label_me", "displayName": "bobby", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label-a", + }, + Object { + "val": "self-label-b", + }, + ], + }, }, ], }, @@ -257,6 +268,17 @@ Object { }, "description": "hi im bob label_me", "displayName": "bobby", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label-a", + }, + Object { + "val": "self-label-b", + }, + ], + }, }, ], }, @@ -377,6 +399,17 @@ Object { }, "description": "hi im bob label_me", "displayName": "bobby", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label-a", + }, + Object { + "val": "self-label-b", + }, + ], + }, }, ], }, @@ -424,6 +457,17 @@ Array [ }, "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", + }, + ], + }, }, ], }, diff --git a/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap index dddb9f6ba6c..2028b5c84a4 100644 --- a/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap @@ -11,7 +11,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -26,12 +43,12 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(3)", - "uri": "record(2)", + "cid": "cids(4)", + "uri": "record(3)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(1)", + "cid": "cids(3)", + "uri": "record(2)", }, }, "text": "thanks bob", @@ -49,41 +66,58 @@ Object { "did": "user(2)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(6)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(6)@jpeg", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(2)", + "uri": "record(3)", "val": "test-label", }, Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(2)", + "uri": "record(3)", "val": "test-label-2", }, ], @@ -100,7 +134,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(6)", }, "size": 4114, }, @@ -109,19 +143,19 @@ Object { }, "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(1)", + "cid": "cids(3)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(1)", + "cid": "cids(3)", + "uri": "record(2)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, "root": Object { @@ -131,13 +165,30 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -148,7 +199,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -161,12 +212,12 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -175,19 +226,19 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(1)", + "cid": "cids(3)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(1)", + "cid": "cids(3)", + "uri": "record(2)", }, }, "text": "of course", }, "replyCount": 0, "repostCount": 0, - "uri": "record(5)", + "uri": "record(7)", "viewer": Object {}, }, "reply": Object { @@ -198,13 +249,30 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -215,7 +283,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, "root": Object { @@ -225,13 +293,30 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -242,7 +327,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -254,41 +339,58 @@ Object { "did": "user(2)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(6)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(6)@jpeg", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(2)", + "uri": "record(3)", "val": "test-label", }, Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(2)", + "uri": "record(3)", "val": "test-label-2", }, ], @@ -305,7 +407,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(6)", }, "size": 4114, }, @@ -314,19 +416,19 @@ Object { }, "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(1)", + "cid": "cids(3)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(1)", + "cid": "cids(3)", + "uri": "record(2)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, "reply": Object { @@ -337,13 +439,30 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -354,7 +473,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, "root": Object { @@ -364,13 +483,30 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -381,7 +517,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -393,13 +529,30 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -410,11 +563,11 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(10)", + "following": "record(12)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", @@ -426,15 +579,15 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(11)", + "uri": "record(13)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -449,7 +602,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(6)", }, "size": 4114, }, @@ -460,7 +613,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(9)", + "$link": "cids(11)", }, "size": 12736, }, @@ -469,8 +622,8 @@ Object { }, "record": Object { "record": Object { - "cid": "cids(10)", - "uri": "record(12)", + "cid": "cids(12)", + "uri": "record(14)", }, }, }, @@ -481,15 +634,15 @@ Object { ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(9)", + "uri": "record(11)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(8)", - "uri": "record(11)", + "cid": "cids(10)", + "uri": "record(13)", }, }, "facets": Array [ @@ -513,11 +666,11 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(8)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(8)", + "uri": "record(10)", "val": "test-label", }, ], @@ -528,15 +681,15 @@ Object { "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(7)", - "uri": "record(9)", + "cid": "cids(9)", + "uri": "record(11)", }, }, "text": "yoohoo label_me", }, "replyCount": 0, "repostCount": 0, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object {}, }, }, @@ -547,15 +700,32 @@ Object { "did": "user(2)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -566,7 +736,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(13)", + "uri": "record(15)", "viewer": Object {}, }, }, @@ -577,13 +747,30 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -594,7 +781,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -606,12 +793,12 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(10)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -619,13 +806,13 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)@jpeg", }, Object { "alt": "tests/image/fixtures/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", + "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", }, ], }, @@ -637,19 +824,36 @@ Object { "did": "user(2)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(12)", "embeds": Array [], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(12)", + "uri": "record(14)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -679,7 +883,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(6)", }, "size": 4114, }, @@ -690,7 +894,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(9)", + "$link": "cids(11)", }, "size": 12736, }, @@ -699,8 +903,8 @@ Object { }, "record": Object { "record": Object { - "cid": "cids(10)", - "uri": "record(12)", + "cid": "cids(12)", + "uri": "record(14)", }, }, }, @@ -708,9 +912,9 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(11)", + "uri": "record(13)", "viewer": Object { - "like": "record(14)", + "like": "record(16)", }, }, }, @@ -721,15 +925,32 @@ Object { "did": "user(2)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -744,7 +965,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(12)", + "uri": "record(14)", "viewer": Object {}, }, }, @@ -755,24 +976,58 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(12)", + "cid": "cids(14)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(14)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(17)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(15)", + "uri": "record(17)", "viewer": Object {}, }, }, diff --git a/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap index 15229efe6cd..399f897bb83 100644 --- a/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap @@ -11,7 +11,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -26,12 +43,12 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(3)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(1)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "thanks bob", @@ -49,15 +66,32 @@ Object { "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -71,19 +105,19 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(2)", + "uri": "record(3)", "val": "test-label", }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(2)", + "uri": "record(3)", "val": "test-label-2", }, ], @@ -100,7 +134,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -109,19 +143,19 @@ Object { }, "reply": Object { "parent": Object { - "cid": "cids(1)", - "uri": "record(1)", + "cid": "cids(2)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(1)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, "root": Object { @@ -131,13 +165,30 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -148,7 +199,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -161,12 +212,12 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -175,19 +226,19 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(1)", - "uri": "record(1)", + "cid": "cids(2)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(1)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "of course", }, "replyCount": 0, "repostCount": 0, - "uri": "record(5)", + "uri": "record(7)", "viewer": Object {}, }, "reply": Object { @@ -198,13 +249,30 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -215,7 +283,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, "root": Object { @@ -225,13 +293,30 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -242,7 +327,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -254,15 +339,32 @@ Object { "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -276,19 +378,19 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(2)", + "uri": "record(3)", "val": "test-label", }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(2)", + "uri": "record(3)", "val": "test-label-2", }, ], @@ -305,7 +407,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -314,19 +416,19 @@ Object { }, "reply": Object { "parent": Object { - "cid": "cids(1)", - "uri": "record(1)", + "cid": "cids(2)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(1)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, "reply": Object { @@ -337,13 +439,30 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -354,7 +473,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, "root": Object { @@ -364,13 +483,30 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -381,7 +517,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -393,13 +529,30 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(7)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -418,11 +571,11 @@ Object { ], "viewer": Object { "blockedBy": false, - "following": "record(10)", + "following": "record(12)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", @@ -434,15 +587,15 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(11)", + "uri": "record(13)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -457,7 +610,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -468,7 +621,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(8)", + "$link": "cids(10)", }, "size": 12736, }, @@ -477,8 +630,8 @@ Object { }, "record": Object { "record": Object { - "cid": "cids(9)", - "uri": "record(12)", + "cid": "cids(11)", + "uri": "record(14)", }, }, }, @@ -489,15 +642,15 @@ Object { ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(9)", + "uri": "record(11)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(7)", - "uri": "record(11)", + "cid": "cids(9)", + "uri": "record(13)", }, }, "facets": Array [ @@ -521,11 +674,11 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(5)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(8)", + "uri": "record(10)", "val": "test-label", }, ], @@ -536,15 +689,15 @@ Object { "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(6)", - "uri": "record(9)", + "cid": "cids(8)", + "uri": "record(11)", }, }, "text": "yoohoo label_me", }, "replyCount": 0, "repostCount": 0, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object {}, }, }, @@ -555,15 +708,32 @@ Object { "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -574,7 +744,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(13)", + "uri": "record(15)", "viewer": Object {}, }, }, @@ -585,13 +755,30 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -602,7 +789,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -614,12 +801,12 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -645,19 +832,36 @@ Object { "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(11)", "embeds": Array [], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(12)", + "uri": "record(14)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -687,7 +891,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -698,7 +902,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(8)", + "$link": "cids(10)", }, "size": 12736, }, @@ -707,8 +911,8 @@ Object { }, "record": Object { "record": Object { - "cid": "cids(9)", - "uri": "record(12)", + "cid": "cids(11)", + "uri": "record(14)", }, }, }, @@ -716,9 +920,9 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(11)", + "uri": "record(13)", "viewer": Object { - "like": "record(14)", + "like": "record(16)", }, }, }, @@ -729,15 +933,32 @@ Object { "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(11)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -752,7 +973,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(12)", + "uri": "record(14)", "viewer": Object {}, }, }, @@ -763,24 +984,58 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(13)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(17)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(15)", + "uri": "record(17)", "viewer": Object {}, }, }, @@ -799,7 +1054,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -835,7 +1107,7 @@ Object { ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, @@ -858,11 +1130,11 @@ Object { ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -873,12 +1145,12 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -905,18 +1177,35 @@ Object { "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(6)", + "uri": "record(7)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -932,7 +1221,7 @@ Object { ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(3)", + "uri": "record(4)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -947,7 +1236,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -958,7 +1247,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 12736, }, @@ -967,8 +1256,8 @@ Object { }, "record": Object { "record": Object { - "cid": "cids(5)", - "uri": "record(6)", + "cid": "cids(6)", + "uri": "record(7)", }, }, }, @@ -985,8 +1274,8 @@ Object { "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "facets": Array [ @@ -1007,7 +1296,7 @@ Object { }, "replyCount": 0, "repostCount": 1, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, "reason": Object { @@ -1018,8 +1307,8 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, @@ -1033,13 +1322,30 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1048,8 +1354,8 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(7)", - "uri": "record(10)", + "cid": "cids(9)", + "uri": "record(12)", }, "root": Object { "cid": "cids(0)", @@ -1060,7 +1366,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(9)", + "uri": "record(11)", "viewer": Object {}, }, "reply": Object { @@ -1071,15 +1377,32 @@ Object { "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -1093,19 +1416,19 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(10)", + "uri": "record(12)", "val": "test-label", }, Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(10)", + "uri": "record(12)", "val": "test-label-2", }, ], @@ -1122,7 +1445,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -1143,7 +1466,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(10)", + "uri": "record(12)", "viewer": Object {}, }, "root": Object { @@ -1153,7 +1476,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1183,12 +1523,12 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1209,7 +1549,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(11)", + "uri": "record(13)", "viewer": Object {}, }, "reply": Object { @@ -1220,7 +1560,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1247,7 +1604,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1276,15 +1650,32 @@ Object { "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -1298,19 +1689,19 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(10)", + "uri": "record(12)", "val": "test-label", }, Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(10)", + "uri": "record(12)", "val": "test-label-2", }, ], @@ -1327,7 +1718,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -1348,7 +1739,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(10)", + "uri": "record(12)", "viewer": Object {}, }, "reply": Object { @@ -1359,7 +1750,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1386,7 +1794,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1415,13 +1840,30 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(11)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -1440,11 +1882,11 @@ Object { ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", @@ -1456,15 +1898,15 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(3)", + "uri": "record(4)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1479,7 +1921,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -1490,7 +1932,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 12736, }, @@ -1499,8 +1941,8 @@ Object { }, "record": Object { "record": Object { - "cid": "cids(5)", - "uri": "record(6)", + "cid": "cids(6)", + "uri": "record(7)", }, }, }, @@ -1511,15 +1953,15 @@ Object { ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(2)", + "uri": "record(3)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "facets": Array [ @@ -1543,11 +1985,11 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(9)", + "cid": "cids(11)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(12)", + "uri": "record(14)", "val": "test-label", }, ], @@ -1558,15 +2000,15 @@ Object { "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(1)", - "uri": "record(2)", + "cid": "cids(2)", + "uri": "record(3)", }, }, "text": "yoohoo label_me", }, "replyCount": 0, "repostCount": 0, - "uri": "record(12)", + "uri": "record(14)", "viewer": Object {}, }, }, @@ -1577,15 +2019,32 @@ Object { "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1596,7 +2055,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(13)", + "uri": "record(15)", "viewer": Object {}, }, }, @@ -1607,7 +2066,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1644,11 +2120,11 @@ Object { ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -1659,12 +2135,12 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -1691,18 +2167,35 @@ Object { "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(6)", + "uri": "record(7)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1718,7 +2211,7 @@ Object { ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(3)", + "uri": "record(4)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1733,7 +2226,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -1744,7 +2237,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 12736, }, @@ -1753,8 +2246,8 @@ Object { }, "record": Object { "record": Object { - "cid": "cids(5)", - "uri": "record(6)", + "cid": "cids(6)", + "uri": "record(7)", }, }, }, @@ -1771,8 +2264,8 @@ Object { "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "facets": Array [ @@ -1793,7 +2286,7 @@ Object { }, "replyCount": 0, "repostCount": 1, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, }, @@ -1813,11 +2306,11 @@ Object { ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1828,7 +2321,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(14)", + "uri": "record(16)", "viewer": Object {}, }, }, @@ -1840,12 +2333,12 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -1871,19 +2364,36 @@ Object { "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "embeds": Array [], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(6)", + "uri": "record(7)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1913,7 +2423,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -1924,7 +2434,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 12736, }, @@ -1933,8 +2443,8 @@ Object { }, "record": Object { "record": Object { - "cid": "cids(5)", - "uri": "record(6)", + "cid": "cids(6)", + "uri": "record(7)", }, }, }, @@ -1942,9 +2452,9 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object { - "like": "record(15)", + "like": "record(17)", }, }, }, @@ -1955,15 +2465,32 @@ Object { "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1978,7 +2505,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(6)", + "uri": "record(7)", "viewer": Object {}, }, }, @@ -1989,24 +2516,58 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(12)", + "cid": "cids(14)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(14)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(18)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(16)", + "uri": "record(18)", "viewer": Object {}, }, }, diff --git a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap index 85269227bbb..65baff8fa50 100644 --- a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap @@ -10,7 +10,24 @@ Object { "followsCount": 2, "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "postsCount": 3, "viewer": Object { "blockedBy": false, @@ -33,7 +50,24 @@ Object { "followsCount": 3, "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label-b", + }, + ], "postsCount": 4, "viewer": Object { "blockedBy": false, @@ -49,12 +83,29 @@ Object { "followsCount": 2, "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "postsCount": 3, "viewer": Object { "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", + "followedBy": "record(2)", + "following": "record(1)", "muted": false, }, }, @@ -72,7 +123,24 @@ Object { "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(0)", @@ -102,7 +170,24 @@ Array [ "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -115,11 +200,28 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", + "followedBy": "record(2)", + "following": "record(1)", "muted": false, }, }, @@ -129,8 +231,8 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, @@ -140,7 +242,7 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(4)", + "following": "record(6)", "muted": false, }, }, @@ -204,7 +306,24 @@ Object { "did": "user(0)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -218,8 +337,8 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/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", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(1)/cids(3)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(1)/cids(3)@jpeg", }, ], }, @@ -255,7 +374,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(2)", + "$link": "cids(3)", }, "size": 4114, }, @@ -264,12 +383,12 @@ Object { }, "reply": Object { "parent": Object { - "cid": "cids(3)", - "uri": "record(3)", + "cid": "cids(4)", + "uri": "record(4)", }, "root": Object { - "cid": "cids(3)", - "uri": "record(3)", + "cid": "cids(4)", + "uri": "record(4)", }, }, "text": "hear that label_me label_me_2", @@ -287,13 +406,30 @@ Object { "did": "user(2)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -304,7 +440,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object {}, }, "root": Object { @@ -314,13 +450,30 @@ Object { "did": "user(2)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -331,7 +484,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object {}, }, }, @@ -343,7 +496,24 @@ Object { "did": "user(0)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -351,7 +521,7 @@ Object { "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -362,7 +532,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(6)", "viewer": Object {}, }, }, @@ -373,7 +543,24 @@ Object { "did": "user(0)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -381,7 +568,7 @@ Object { "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -396,7 +583,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(5)", + "uri": "record(7)", "viewer": Object {}, }, }, @@ -415,7 +602,24 @@ Object { "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -441,7 +645,24 @@ Object { "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -470,7 +691,24 @@ Object { "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -489,7 +727,24 @@ Object { "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(4)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(4)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -512,7 +767,24 @@ Object { "did": "user(0)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -545,12 +817,12 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -558,13 +830,13 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/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", + "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", }, Object { "alt": "tests/image/fixtures/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", + "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", }, ], }, @@ -576,7 +848,24 @@ Object { "did": "user(0)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -618,7 +907,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -629,7 +918,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 12736, }, @@ -647,9 +936,9 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object { - "like": "record(6)", + "like": "record(7)", }, }, ], @@ -1884,7 +2173,24 @@ Object { "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "blocking": "record(3)", @@ -1908,7 +2214,7 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(3)", "muted": false, }, }, @@ -1919,7 +2225,24 @@ Object { "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(4)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(4)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1933,7 +2256,24 @@ Object { "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -1954,8 +2294,8 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, @@ -1966,7 +2306,24 @@ Object { "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1980,7 +2337,24 @@ Object { "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -2016,7 +2390,24 @@ Object { "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -2025,7 +2416,7 @@ Object { }, ], "list": Object { - "cid": "cids(1)", + "cid": "cids(2)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(0)@jpeg", "description": "hi im bob label_me", @@ -2033,11 +2424,28 @@ Object { "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, @@ -2045,7 +2453,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "name": "bob mutes", "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(2)", + "uri": "record(3)", "viewer": Object { "muted": false, }, @@ -2066,7 +2474,24 @@ Object { "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -2084,7 +2509,7 @@ Object { }, }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "description": "hi im bob label_me", @@ -2092,7 +2517,24 @@ Object { "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -2104,7 +2546,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "name": "bob mutes", "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(3)", + "uri": "record(4)", "viewer": Object { "muted": false, }, diff --git a/packages/pds/tests/seeds/basic.ts b/packages/pds/tests/seeds/basic.ts index 45de50f322e..464dd832975 100644 --- a/packages/pds/tests/seeds/basic.ts +++ b/packages/pds/tests/seeds/basic.ts @@ -25,7 +25,12 @@ export default async (sc: SeedClient, invite?: { code: string }) => { await sc.follow(bob, alice) await sc.follow(bob, carol, createdAtMicroseconds()) await sc.follow(dan, bob, createdAtTimezone()) - await sc.post(alice, posts.alice[0]) + await sc.post(alice, posts.alice[0], undefined, undefined, undefined, { + labels: { + $type: 'com.atproto.label.defs#selfLabels', + values: [{ val: 'self-label' }], + }, + }) await sc.post(bob, posts.bob[0], undefined, undefined, undefined, { langs: ['en-US', 'i-klingon'], }) diff --git a/packages/pds/tests/seeds/client.ts b/packages/pds/tests/seeds/client.ts index c5cbcd0a5b5..82cb02571eb 100644 --- a/packages/pds/tests/seeds/client.ts +++ b/packages/pds/tests/seeds/client.ts @@ -121,7 +121,7 @@ export class SeedClient { by: string, displayName: string, description: string, - fromUser?: string, + selfLabels?: string[], ) { AVATAR_IMG ??= await fs.readFile( 'tests/image/fixtures/key-portrait-small.jpg', @@ -131,7 +131,7 @@ export class SeedClient { { const res = await this.agent.api.com.atproto.repo.uploadBlob(AVATAR_IMG, { encoding: 'image/jpeg', - headers: this.getHeaders(fromUser || by), + headers: this.getHeaders(by), } as any) avatarBlob = res.data.blob } @@ -143,8 +143,14 @@ export class SeedClient { displayName, description, avatar: avatarBlob, + labels: selfLabels + ? { + $type: 'com.atproto.label.defs#selfLabels', + values: selfLabels.map((val) => ({ val })), + } + : undefined, }, - this.getHeaders(fromUser || by), + this.getHeaders(by), ) this.profiles[by] = { displayName, diff --git a/packages/pds/tests/seeds/users.ts b/packages/pds/tests/seeds/users.ts index c94cd88817f..2ef9a74864f 100644 --- a/packages/pds/tests/seeds/users.ts +++ b/packages/pds/tests/seeds/users.ts @@ -10,11 +10,13 @@ export default async (sc: SeedClient, invite?: { code: string }) => { sc.dids.alice, users.alice.displayName, users.alice.description, + users.alice.selfLabels, ) await sc.createProfile( sc.dids.bob, users.bob.displayName, users.bob.description, + users.alice.selfLabels, ) return sc @@ -27,6 +29,7 @@ const users = { password: 'alice-pass', displayName: 'ali', description: 'its me!', + selfLabels: ['self-label-a', 'self-label-b'], }, bob: { email: 'bob@test.com', @@ -34,6 +37,7 @@ const users = { password: 'bob-pass', displayName: 'bobby', description: 'hi im bob label_me', + selfLabels: undefined, }, carol: { email: 'carol@test.com', @@ -41,6 +45,7 @@ const users = { password: 'carol-pass', displayName: undefined, description: undefined, + selfLabels: undefined, }, dan: { email: 'dan@test.com', @@ -48,5 +53,6 @@ const users = { password: 'dan-pass', displayName: undefined, description: undefined, + selfLabels: undefined, }, } diff --git a/packages/pds/tests/views/__snapshots__/author-feed.test.ts.snap b/packages/pds/tests/views/__snapshots__/author-feed.test.ts.snap index adc5260596e..485ba8c2b1a 100644 --- a/packages/pds/tests/views/__snapshots__/author-feed.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/author-feed.test.ts.snap @@ -9,7 +9,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -24,12 +41,12 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(3)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(1)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "thanks bob", @@ -47,15 +64,32 @@ Array [ "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -69,19 +103,19 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(2)", + "uri": "record(3)", "val": "test-label", }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(2)", + "uri": "record(3)", "val": "test-label-2", }, ], @@ -98,7 +132,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -107,19 +141,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(1)", - "uri": "record(1)", + "cid": "cids(2)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(1)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, "root": Object { @@ -129,13 +163,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -146,7 +197,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -158,13 +209,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(6)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -183,11 +251,11 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(7)", + "following": "record(9)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(7)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", @@ -199,15 +267,15 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", + "followedBy": "record(12)", + "following": "record(11)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(8)", + "uri": "record(10)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -222,7 +290,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -233,7 +301,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(7)", + "$link": "cids(9)", }, "size": 12736, }, @@ -242,8 +310,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(8)", - "uri": "record(11)", + "cid": "cids(10)", + "uri": "record(13)", }, }, }, @@ -254,15 +322,15 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(6)", + "uri": "record(8)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(10)", }, }, "facets": Array [ @@ -286,11 +354,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(4)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(5)", + "uri": "record(7)", "val": "test-label", }, ], @@ -301,15 +369,15 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(5)", - "uri": "record(6)", + "cid": "cids(7)", + "uri": "record(8)", }, }, "text": "yoohoo label_me", }, "replyCount": 0, "repostCount": 0, - "uri": "record(5)", + "uri": "record(7)", "viewer": Object {}, }, }, @@ -320,13 +388,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -337,7 +422,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -348,24 +433,58 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(11)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(11)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(14)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(12)", + "uri": "record(14)", "viewer": Object {}, }, }, @@ -381,7 +500,24 @@ Array [ "did": "user(0)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -430,7 +566,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(1)", + "$link": "cids(2)", }, "size": 4114, }, @@ -439,12 +575,12 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(1)", + "cid": "cids(3)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(1)", + "cid": "cids(3)", + "uri": "record(2)", }, }, "text": "hear that label_me label_me_2", @@ -462,15 +598,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -481,9 +634,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object { - "like": "record(4)", + "like": "record(6)", }, }, "root": Object { @@ -493,15 +646,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -512,9 +682,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object { - "like": "record(4)", + "like": "record(6)", }, }, }, @@ -526,13 +696,30 @@ Array [ "did": "user(0)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(5)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -543,7 +730,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(5)", + "uri": "record(7)", "viewer": Object {}, }, }, @@ -554,13 +741,30 @@ Array [ "did": "user(0)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -575,7 +779,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(6)", + "uri": "record(8)", "viewer": Object {}, }, }, @@ -644,7 +848,24 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(4)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(4)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(3)", @@ -747,7 +968,7 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { - "repost": "record(4)", + "repost": "record(5)", }, }, "reason": Object { @@ -775,7 +996,7 @@ Array [ "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -784,19 +1005,19 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(6)", - "uri": "record(6)", + "cid": "cids(7)", + "uri": "record(7)", }, "root": Object { - "cid": "cids(6)", - "uri": "record(6)", + "cid": "cids(7)", + "uri": "record(7)", }, }, "text": "of course", }, "replyCount": 0, "repostCount": 0, - "uri": "record(5)", + "uri": "record(6)", "viewer": Object {}, }, "reply": Object { @@ -807,15 +1028,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(8)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(8)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -826,9 +1064,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(6)", + "uri": "record(7)", "viewer": Object { - "like": "record(9)", + "like": "record(11)", }, }, "root": Object { @@ -838,15 +1076,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(8)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(8)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -857,9 +1112,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(6)", + "uri": "record(7)", "viewer": Object { - "like": "record(9)", + "like": "record(11)", }, }, }, @@ -901,7 +1156,24 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(4)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(4)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(3)", @@ -987,7 +1259,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -1007,8 +1296,8 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", - "repost": "record(2)", + "like": "record(4)", + "repost": "record(3)", }, }, "reason": Object { @@ -1052,7 +1341,7 @@ Array [ "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -1066,7 +1355,7 @@ Array [ "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -1093,17 +1382,34 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "following": "record(7)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(6)", + "uri": "record(7)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1119,7 +1425,7 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(5)", + "uri": "record(6)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1134,7 +1440,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -1145,7 +1451,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 12736, }, @@ -1154,8 +1460,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(5)", - "uri": "record(6)", + "cid": "cids(6)", + "uri": "record(7)", }, }, }, @@ -1172,8 +1478,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(2)", - "uri": "record(5)", + "cid": "cids(3)", + "uri": "record(6)", }, }, "facets": Array [ @@ -1194,7 +1500,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, }, @@ -1217,7 +1523,7 @@ Array [ "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1228,7 +1534,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object {}, }, }, @@ -1299,7 +1605,24 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1423,7 +1746,7 @@ Array [ "muted": true, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1434,7 +1757,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(5)", + "uri": "record(6)", "viewer": Object {}, }, }, @@ -1450,7 +1773,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1467,12 +1807,12 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(4)", + "cid": "cids(3)", + "uri": "record(5)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(3)", + "cid": "cids(2)", + "uri": "record(4)", }, }, "text": "thanks bob", @@ -1490,14 +1830,31 @@ Array [ "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", + "followedBy": "record(7)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -1511,19 +1868,19 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label", }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label-2", }, ], @@ -1540,7 +1897,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1549,19 +1906,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(1)", - "uri": "record(3)", + "cid": "cids(2)", + "uri": "record(4)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(3)", + "cid": "cids(2)", + "uri": "record(4)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "root": Object { @@ -1571,7 +1928,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1579,7 +1953,7 @@ Array [ "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1590,9 +1964,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object { - "like": "record(5)", + "like": "record(6)", }, }, }, @@ -1604,7 +1978,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1612,7 +2003,7 @@ Array [ "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(6)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -1634,7 +2025,7 @@ Array [ "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(7)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", @@ -1649,10 +2040,10 @@ Array [ "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(9)", + "uri": "record(11)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1667,7 +2058,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1678,7 +2069,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(7)", + "$link": "cids(9)", }, "size": 12736, }, @@ -1687,8 +2078,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(8)", - "uri": "record(10)", + "cid": "cids(10)", + "uri": "record(12)", }, }, }, @@ -1699,15 +2090,15 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(8)", + "uri": "record(10)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(6)", - "uri": "record(9)", + "cid": "cids(8)", + "uri": "record(11)", }, }, "facets": Array [ @@ -1731,11 +2122,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(4)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(7)", + "uri": "record(9)", "val": "test-label", }, ], @@ -1746,17 +2137,17 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(5)", - "uri": "record(8)", + "cid": "cids(7)", + "uri": "record(10)", }, }, "text": "yoohoo label_me", }, "replyCount": 0, "repostCount": 0, - "uri": "record(7)", + "uri": "record(9)", "viewer": Object { - "like": "record(11)", + "like": "record(13)", }, }, }, @@ -1767,7 +2158,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1775,7 +2183,7 @@ Array [ "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1786,9 +2194,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object { - "like": "record(5)", + "like": "record(6)", }, }, }, @@ -1799,7 +2207,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1807,18 +2232,35 @@ Array [ "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(11)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(11)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(14)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(12)", + "uri": "record(14)", "viewer": Object {}, }, }, diff --git a/packages/pds/tests/views/__snapshots__/blocks.test.ts.snap b/packages/pds/tests/views/__snapshots__/blocks.test.ts.snap index 8c8e3bf3b7e..d29d658bc5d 100644 --- a/packages/pds/tests/views/__snapshots__/blocks.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/blocks.test.ts.snap @@ -10,7 +10,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -39,27 +56,27 @@ Object { "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewBlocked", - "uri": "record(3)", + "uri": "record(4)", }, }, ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(2)", + "uri": "record(3)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "facets": Array [ @@ -98,8 +115,8 @@ Object { "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(1)", - "uri": "record(2)", + "cid": "cids(2)", + "uri": "record(3)", }, }, "text": "yoohoo label_me", @@ -120,7 +137,7 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#blockedPost", "blocked": true, - "uri": "record(3)", + "uri": "record(4)", }, "post": Object { "author": Object { @@ -128,7 +145,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -145,12 +179,12 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(1)", - "uri": "record(3)", + "cid": "cids(2)", + "uri": "record(4)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(3)", + "cid": "cids(2)", + "uri": "record(4)", }, }, "text": "alice replies to dan", @@ -175,7 +209,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -195,15 +246,15 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", - "repost": "record(2)", + "like": "record(4)", + "repost": "record(3)", }, }, "replies": Array [ Object { "$type": "app.bsky.feed.defs#blockedPost", "blocked": true, - "uri": "record(4)", + "uri": "record(5)", }, Object { "$type": "app.bsky.feed.defs#threadViewPost", @@ -213,14 +264,31 @@ Object { "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "following": "record(6)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -234,19 +302,19 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(1)", + "cid": "cids(2)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(5)", + "uri": "record(6)", "val": "test-label", }, Object { - "cid": "cids(1)", + "cid": "cids(2)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(5)", + "uri": "record(6)", "val": "test-label-2", }, ], @@ -263,7 +331,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(2)", + "$link": "cids(4)", }, "size": 4114, }, @@ -284,7 +352,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(5)", + "uri": "record(6)", "viewer": Object {}, }, }, diff --git a/packages/pds/tests/views/__snapshots__/likes.test.ts.snap b/packages/pds/tests/views/__snapshots__/likes.test.ts.snap index 25a4518111e..3a15355409f 100644 --- a/packages/pds/tests/views/__snapshots__/likes.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/likes.test.ts.snap @@ -62,7 +62,24 @@ Object { "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(5)", diff --git a/packages/pds/tests/views/__snapshots__/mute-lists.test.ts.snap b/packages/pds/tests/views/__snapshots__/mute-lists.test.ts.snap index aa888736d6d..6434e1e51b1 100644 --- a/packages/pds/tests/views/__snapshots__/mute-lists.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/mute-lists.test.ts.snap @@ -7,7 +7,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -18,7 +35,7 @@ Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.graph.defs#listView", - "cid": "cids(2)", + "cid": "cids(3)", "creator": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", "did": "user(0)", @@ -33,7 +50,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "name": "updated alice mutes", "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(1)", + "uri": "record(2)", "viewer": Object { "muted": false, }, @@ -48,8 +65,8 @@ Object { "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(1)", - "uri": "record(1)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "list embed!", @@ -70,7 +87,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -90,8 +124,8 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", - "repost": "record(2)", + "like": "record(4)", + "repost": "record(3)", }, }, "replies": Array [ @@ -104,23 +138,23 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": true, "mutedByList": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "name": "alice mutes", "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(5)", + "uri": "record(6)", "viewer": Object { "muted": true, }, }, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -141,7 +175,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, }, @@ -153,25 +187,42 @@ Object { "did": "user(2)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "following": "record(9)", + "following": "record(10)", "muted": true, "mutedByList": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "name": "alice mutes", "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(5)", + "uri": "record(6)", "viewer": Object { "muted": true, }, }, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -185,19 +236,19 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(8)", + "uri": "record(9)", "val": "test-label", }, Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(8)", + "uri": "record(9)", "val": "test-label-2", }, ], @@ -214,7 +265,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(6)", }, "size": 4114, }, @@ -235,7 +286,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object {}, }, }, @@ -256,7 +307,24 @@ Object { "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -274,7 +342,7 @@ Object { }, Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "cid": "cids(1)", + "cid": "cids(2)", "creator": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", "description": "its me!", @@ -282,7 +350,24 @@ Object { "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -293,7 +378,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "name": "alice mutes", "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(2)", + "uri": "record(3)", "viewer": Object { "muted": true, }, @@ -315,7 +400,24 @@ Object { "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -333,7 +435,7 @@ Object { }, Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "cid": "cids(1)", + "cid": "cids(2)", "creator": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", "description": "its me!", @@ -341,7 +443,24 @@ Object { "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -352,7 +471,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "name": "alice mutes", "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(2)", + "uri": "record(3)", "viewer": Object { "muted": true, }, @@ -396,7 +515,24 @@ Object { "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "following": "record(2)", @@ -426,10 +562,27 @@ Object { "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", + "followedBy": "record(4)", "muted": false, }, }, diff --git a/packages/pds/tests/views/__snapshots__/notifications.test.ts.snap b/packages/pds/tests/views/__snapshots__/notifications.test.ts.snap index 287b6ba633a..bcf1adfe6d3 100644 --- a/packages/pds/tests/views/__snapshots__/notifications.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/notifications.test.ts.snap @@ -80,7 +80,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(9)", @@ -123,7 +140,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(6)", }, "size": 4114, }, @@ -163,7 +180,7 @@ Array [ "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -177,7 +194,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(10)", + "uri": "record(11)", }, Object { "author": Object { @@ -191,21 +208,21 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "like", - "reasonSubject": "record(12)", + "reasonSubject": "record(13)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(8)", - "uri": "record(12)", + "cid": "cids(9)", + "uri": "record(13)", }, }, - "uri": "record(11)", + "uri": "record(12)", }, Object { "author": Object { @@ -219,7 +236,7 @@ Array [ "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -233,7 +250,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(13)", + "uri": "record(14)", }, Object { "author": Object { @@ -243,7 +260,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(9)", @@ -251,21 +285,21 @@ Array [ "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(11)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "like", - "reasonSubject": "record(12)", + "reasonSubject": "record(13)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(8)", - "uri": "record(12)", + "cid": "cids(9)", + "uri": "record(13)", }, }, - "uri": "record(14)", + "uri": "record(15)", }, Object { "author": Object { @@ -275,7 +309,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(9)", @@ -283,7 +334,7 @@ Array [ "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -297,7 +348,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(15)", + "uri": "record(16)", }, Object { "author": Object { @@ -307,7 +358,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(9)", @@ -315,7 +383,7 @@ Array [ "muted": false, }, }, - "cid": "cids(12)", + "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -339,7 +407,7 @@ Array [ "muted": false, }, }, - "cid": "cids(13)", + "cid": "cids(14)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -364,7 +432,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -394,7 +479,7 @@ Array [ }, ], "reason": "reply", - "reasonSubject": "record(3)", + "reasonSubject": "record(4)", "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -407,7 +492,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(1)", + "$link": "cids(2)", }, "size": 4114, }, @@ -416,12 +501,12 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, "root": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "text": "hear that label_me label_me_2", @@ -436,7 +521,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -444,21 +546,21 @@ Array [ "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "like", - "reasonSubject": "record(5)", + "reasonSubject": "record(6)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(4)", - "uri": "record(5)", + "cid": "cids(5)", + "uri": "record(6)", }, }, - "uri": "record(4)", + "uri": "record(5)", }, Object { "author": Object { @@ -468,7 +570,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -476,21 +595,21 @@ Array [ "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "like", - "reasonSubject": "record(3)", + "reasonSubject": "record(4)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, - "uri": "record(6)", + "uri": "record(7)", }, Object { "author": Object { @@ -500,7 +619,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -508,7 +644,7 @@ Array [ "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -638,7 +774,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-a", + }, + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(10)", @@ -681,7 +834,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(6)", + "$link": "cids(7)", }, "size": 4114, }, @@ -721,7 +874,7 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [], @@ -735,7 +888,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(11)", + "uri": "record(12)", }, Object { "author": Object { @@ -749,21 +902,21 @@ Array [ "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [], "reason": "like", - "reasonSubject": "record(13)", + "reasonSubject": "record(14)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(9)", - "uri": "record(13)", + "cid": "cids(10)", + "uri": "record(14)", }, }, - "uri": "record(12)", + "uri": "record(13)", }, Object { "author": Object { @@ -777,7 +930,7 @@ Array [ "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(11)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [], @@ -791,7 +944,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(14)", + "uri": "record(15)", }, Object { "author": Object { @@ -801,7 +954,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-a", + }, + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(10)", @@ -809,21 +979,21 @@ Array [ "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [], "reason": "like", - "reasonSubject": "record(13)", + "reasonSubject": "record(14)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(9)", - "uri": "record(13)", + "cid": "cids(10)", + "uri": "record(14)", }, }, - "uri": "record(15)", + "uri": "record(16)", }, Object { "author": Object { @@ -833,7 +1003,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-a", + }, + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(10)", @@ -841,7 +1028,7 @@ Array [ "muted": false, }, }, - "cid": "cids(12)", + "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [], @@ -855,7 +1042,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(16)", + "uri": "record(17)", }, Object { "author": Object { @@ -876,7 +1063,7 @@ Array [ "muted": false, }, }, - "cid": "cids(13)", + "cid": "cids(14)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [], @@ -887,8 +1074,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(14)", - "uri": "record(18)", + "cid": "cids(15)", + "uri": "record(19)", }, }, "facets": Array [ @@ -907,7 +1094,7 @@ Array [ ], "text": "@alice.bluesky.xyz is the best", }, - "uri": "record(17)", + "uri": "record(18)", }, Object { "author": Object { @@ -917,7 +1104,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-a", + }, + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(10)", @@ -925,7 +1129,7 @@ Array [ "muted": false, }, }, - "cid": "cids(15)", + "cid": "cids(16)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [], @@ -949,7 +1153,7 @@ Array [ "muted": false, }, }, - "cid": "cids(16)", + "cid": "cids(17)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [], @@ -1079,7 +1283,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-a", + }, + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(10)", @@ -1122,7 +1343,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(6)", + "$link": "cids(7)", }, "size": 4114, }, @@ -1162,7 +1383,7 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -1176,7 +1397,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(11)", + "uri": "record(12)", }, Object { "author": Object { @@ -1190,21 +1411,21 @@ Array [ "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "like", - "reasonSubject": "record(13)", + "reasonSubject": "record(14)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(9)", - "uri": "record(13)", + "cid": "cids(10)", + "uri": "record(14)", }, }, - "uri": "record(12)", + "uri": "record(13)", }, Object { "author": Object { @@ -1218,7 +1439,7 @@ Array [ "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(11)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -1232,7 +1453,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(14)", + "uri": "record(15)", }, Object { "author": Object { @@ -1242,7 +1463,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-a", + }, + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(10)", @@ -1250,21 +1488,21 @@ Array [ "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "like", - "reasonSubject": "record(13)", + "reasonSubject": "record(14)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(9)", - "uri": "record(13)", + "cid": "cids(10)", + "uri": "record(14)", }, }, - "uri": "record(15)", + "uri": "record(16)", }, Object { "author": Object { @@ -1274,7 +1512,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-a", + }, + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(10)", @@ -1282,7 +1537,7 @@ Array [ "muted": false, }, }, - "cid": "cids(12)", + "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -1296,7 +1551,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(16)", + "uri": "record(17)", }, Object { "author": Object { @@ -1317,7 +1572,7 @@ Array [ "muted": false, }, }, - "cid": "cids(13)", + "cid": "cids(14)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -1328,8 +1583,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(14)", - "uri": "record(18)", + "cid": "cids(15)", + "uri": "record(19)", }, }, "facets": Array [ @@ -1348,7 +1603,7 @@ Array [ ], "text": "@alice.bluesky.xyz is the best", }, - "uri": "record(17)", + "uri": "record(18)", }, Object { "author": Object { @@ -1358,7 +1613,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-a", + }, + Object { + "cid": "cids(6)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(11)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(10)", @@ -1366,7 +1638,7 @@ Array [ "muted": false, }, }, - "cid": "cids(15)", + "cid": "cids(16)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -1390,7 +1662,7 @@ Array [ "muted": false, }, }, - "cid": "cids(16)", + "cid": "cids(17)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -1443,7 +1715,24 @@ Object { "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(4)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(4)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(3)", @@ -1487,14 +1776,31 @@ Object { "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(4)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(4)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(3)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], diff --git a/packages/pds/tests/views/__snapshots__/posts.test.ts.snap b/packages/pds/tests/views/__snapshots__/posts.test.ts.snap index fd50b6851c9..311d2665430 100644 --- a/packages/pds/tests/views/__snapshots__/posts.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/posts.test.ts.snap @@ -8,7 +8,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -16,11 +33,28 @@ Array [ }, "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, @@ -34,13 +68,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -51,7 +102,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(1)", + "uri": "record(2)", "viewer": Object {}, }, Object { @@ -60,15 +111,32 @@ Array [ "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -83,7 +151,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, Object { @@ -93,12 +161,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(5)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -124,19 +192,36 @@ Array [ "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embeds": Array [], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(2)", + "uri": "record(3)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -166,7 +251,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(6)", }, "size": 4114, }, @@ -177,7 +262,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(7)", }, "size": 12736, }, @@ -186,8 +271,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(2)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(3)", }, }, }, @@ -195,9 +280,9 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(5)", + "uri": "record(7)", "viewer": Object { - "like": "record(8)", + "like": "record(10)", }, }, Object { @@ -215,11 +300,11 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(10)", + "following": "record(12)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -230,12 +315,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(5)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -262,18 +347,35 @@ Array [ "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(2)", + "uri": "record(3)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -289,7 +391,7 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(5)", + "uri": "record(7)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -304,7 +406,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(6)", }, "size": 4114, }, @@ -315,7 +417,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(7)", }, "size": 12736, }, @@ -324,8 +426,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(2)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(3)", }, }, }, @@ -342,8 +444,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(3)", - "uri": "record(5)", + "cid": "cids(5)", + "uri": "record(7)", }, }, "facets": Array [ @@ -364,7 +466,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(9)", + "uri": "record(11)", "viewer": Object {}, }, Object { @@ -373,13 +475,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -388,19 +507,19 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(8)", - "uri": "record(12)", + "cid": "cids(10)", + "uri": "record(14)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(1)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "thanks bob", }, "replyCount": 0, "repostCount": 0, - "uri": "record(11)", + "uri": "record(13)", "viewer": Object {}, }, ] diff --git a/packages/pds/tests/views/__snapshots__/profile.test.ts.snap b/packages/pds/tests/views/__snapshots__/profile.test.ts.snap index eba5c1c17eb..dbfd30bdbce 100644 --- a/packages/pds/tests/views/__snapshots__/profile.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/profile.test.ts.snap @@ -36,7 +36,24 @@ Array [ "followsCount": 3, "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "postsCount": 4, "viewer": Object { "blockedBy": false, @@ -54,7 +71,24 @@ Array [ "followsCount": 2, "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "postsCount": 3, "viewer": Object { "blockedBy": false, @@ -70,7 +104,7 @@ Array [ "postsCount": 2, "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -91,7 +125,7 @@ Array [ "postsCount": 2, "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", + "followedBy": "record(5)", "muted": false, }, }, @@ -108,7 +142,24 @@ Object { "followsCount": 3, "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "postsCount": 4, "viewer": Object { "blockedBy": false, @@ -153,7 +204,24 @@ Object { "followsCount": 3, "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label-a", + }, + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label-b", + }, + ], "postsCount": 4, "viewer": Object { "blockedBy": false, diff --git a/packages/pds/tests/views/__snapshots__/reposts.test.ts.snap b/packages/pds/tests/views/__snapshots__/reposts.test.ts.snap index fc0beba5d9d..69927a4b5ae 100644 --- a/packages/pds/tests/views/__snapshots__/reposts.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/reposts.test.ts.snap @@ -47,7 +47,24 @@ Array [ "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(4)", diff --git a/packages/pds/tests/views/__snapshots__/thread.test.ts.snap b/packages/pds/tests/views/__snapshots__/thread.test.ts.snap index b247e0b12f8..195d40db206 100644 --- a/packages/pds/tests/views/__snapshots__/thread.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/thread.test.ts.snap @@ -6,7 +6,7 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#notFoundPost", "notFound": true, - "uri": "record(4)", + "uri": "record(5)", }, "post": Object { "author": Object { @@ -14,7 +14,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -31,12 +48,12 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(4)", + "cid": "cids(3)", + "uri": "record(5)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(3)", + "cid": "cids(2)", + "uri": "record(4)", }, }, "text": "thanks bob", @@ -45,7 +62,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "repost": "record(5)", + "repost": "record(6)", }, }, "replies": Array [], @@ -58,7 +75,7 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#notFoundPost", "notFound": true, - "uri": "record(4)", + "uri": "record(5)", }, "post": Object { "author": Object { @@ -66,7 +83,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -83,12 +117,12 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(4)", + "cid": "cids(3)", + "uri": "record(5)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(3)", + "cid": "cids(2)", + "uri": "record(4)", }, }, "text": "thanks bob", @@ -97,7 +131,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "repost": "record(5)", + "repost": "record(6)", }, }, "replies": Array [], @@ -113,7 +147,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -134,7 +185,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", + "like": "record(4)", }, }, "replies": Array [ @@ -146,13 +197,30 @@ Object { "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -166,19 +234,19 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(1)", + "cid": "cids(2)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label", }, Object { - "cid": "cids(1)", + "cid": "cids(2)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label-2", }, ], @@ -195,7 +263,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(2)", + "$link": "cids(4)", }, "size": 4114, }, @@ -216,7 +284,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "replies": Array [ @@ -228,7 +296,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -236,7 +321,7 @@ Object { "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(5)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -245,8 +330,8 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(1)", - "uri": "record(4)", + "cid": "cids(2)", + "uri": "record(5)", }, "root": Object { "cid": "cids(0)", @@ -257,9 +342,9 @@ Object { }, "replyCount": 0, "repostCount": 1, - "uri": "record(5)", + "uri": "record(7)", "viewer": Object { - "repost": "record(6)", + "repost": "record(8)", }, }, "replies": Array [], @@ -279,7 +364,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -300,7 +402,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", + "like": "record(4)", }, }, "replies": Array [ @@ -312,13 +414,30 @@ Object { "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(6)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -332,19 +451,19 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(1)", + "cid": "cids(2)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label", }, Object { - "cid": "cids(1)", + "cid": "cids(2)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label-2", }, ], @@ -361,7 +480,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(2)", + "$link": "cids(4)", }, "size": 4114, }, @@ -382,7 +501,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "replies": Array [], @@ -404,7 +523,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -412,7 +548,7 @@ Object { "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -423,9 +559,9 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object { - "like": "record(6)", + "like": "record(8)", }, }, "replies": Array [], @@ -436,13 +572,30 @@ Object { "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(7)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(7)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -456,19 +609,19 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label", }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label-2", }, ], @@ -485,7 +638,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -494,19 +647,19 @@ Object { }, "reply": Object { "parent": Object { - "cid": "cids(1)", - "uri": "record(3)", + "cid": "cids(2)", + "uri": "record(4)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(3)", + "cid": "cids(2)", + "uri": "record(4)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "replies": Array [], @@ -517,7 +670,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -534,12 +704,12 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(4)", + "cid": "cids(3)", + "uri": "record(5)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(3)", + "cid": "cids(2)", + "uri": "record(4)", }, }, "text": "thanks bob", @@ -548,7 +718,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "repost": "record(5)", + "repost": "record(6)", }, }, "replies": Array [], @@ -564,7 +734,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -585,7 +772,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", + "like": "record(4)", }, }, "replies": Array [ @@ -598,11 +785,11 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(5)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -623,7 +810,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "replies": Array [], @@ -636,13 +823,30 @@ Object { "did": "user(2)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -656,19 +860,19 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "test-label", }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "test-label-2", }, ], @@ -685,7 +889,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -706,7 +910,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(6)", + "uri": "record(7)", "viewer": Object {}, }, "replies": Array [ @@ -718,7 +922,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -726,7 +947,7 @@ Object { "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -735,8 +956,8 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(6)", + "cid": "cids(3)", + "uri": "record(7)", }, "root": Object { "cid": "cids(0)", @@ -747,9 +968,9 @@ Object { }, "replyCount": 0, "repostCount": 1, - "uri": "record(7)", + "uri": "record(9)", "viewer": Object { - "repost": "record(8)", + "repost": "record(10)", }, }, "replies": Array [], @@ -771,13 +992,30 @@ Object { "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(7)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(7)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -791,19 +1029,19 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label", }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label-2", }, ], @@ -820,7 +1058,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -829,19 +1067,19 @@ Object { }, "reply": Object { "parent": Object { - "cid": "cids(1)", - "uri": "record(3)", + "cid": "cids(2)", + "uri": "record(4)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(3)", + "cid": "cids(2)", + "uri": "record(4)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "replies": Array [], @@ -852,7 +1090,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -869,12 +1124,12 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(4)", + "cid": "cids(3)", + "uri": "record(5)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(3)", + "cid": "cids(2)", + "uri": "record(4)", }, }, "text": "thanks bob", @@ -883,7 +1138,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "repost": "record(5)", + "repost": "record(6)", }, }, "replies": Array [], @@ -899,7 +1154,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -920,7 +1192,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", + "like": "record(4)", }, }, "replies": Array [ @@ -933,11 +1205,11 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(5)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -958,7 +1230,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, }, @@ -970,13 +1242,30 @@ Object { "did": "user(2)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -990,19 +1279,19 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "test-label", }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "test-label-2", }, ], @@ -1019,7 +1308,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1040,7 +1329,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(6)", + "uri": "record(7)", "viewer": Object {}, }, }, @@ -1057,7 +1346,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1088,13 +1394,30 @@ Object { "did": "user(1)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1115,7 +1438,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object {}, }, "replies": Array [ @@ -1127,7 +1450,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1135,7 +1475,7 @@ Object { "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(4)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1144,8 +1484,8 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(1)", - "uri": "record(3)", + "cid": "cids(2)", + "uri": "record(4)", }, "root": Object { "cid": "cids(0)", @@ -1156,7 +1496,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(6)", "viewer": Object {}, }, "replies": Array [], @@ -1176,7 +1516,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1208,7 +1565,7 @@ Object { "parent": Object { "$type": "app.bsky.feed.defs#notFoundPost", "notFound": true, - "uri": "record(4)", + "uri": "record(5)", }, "post": Object { "author": Object { @@ -1216,7 +1573,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1233,12 +1607,12 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(4)", + "cid": "cids(3)", + "uri": "record(5)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(3)", + "cid": "cids(2)", + "uri": "record(4)", }, }, "text": "Reply reply", @@ -1261,7 +1635,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1282,7 +1673,7 @@ Object { "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", + "like": "record(4)", }, }, "replies": Array [ @@ -1295,11 +1686,11 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(5)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1320,7 +1711,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "replies": Array [], @@ -1333,13 +1724,30 @@ Object { "did": "user(2)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(8)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(8)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -1353,19 +1761,19 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "test-label", }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "test-label-2", }, ], @@ -1382,7 +1790,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1403,7 +1811,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(6)", + "uri": "record(7)", "viewer": Object {}, }, "replies": Array [ @@ -1415,7 +1823,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(2)", @@ -1423,7 +1848,7 @@ Object { "muted": true, }, }, - "cid": "cids(4)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1432,8 +1857,8 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(6)", + "cid": "cids(3)", + "uri": "record(7)", }, "root": Object { "cid": "cids(0)", @@ -1444,9 +1869,9 @@ Object { }, "replyCount": 0, "repostCount": 1, - "uri": "record(7)", + "uri": "record(9)", "viewer": Object { - "repost": "record(8)", + "repost": "record(10)", }, }, "replies": Array [], diff --git a/packages/pds/tests/views/__snapshots__/timeline.test.ts.snap b/packages/pds/tests/views/__snapshots__/timeline.test.ts.snap index 87f68a25129..a818274c1a3 100644 --- a/packages/pds/tests/views/__snapshots__/timeline.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/timeline.test.ts.snap @@ -9,7 +9,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -45,7 +62,7 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, @@ -59,13 +76,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -74,8 +108,8 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, "root": Object { "cid": "cids(0)", @@ -86,7 +120,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, }, @@ -97,13 +131,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -122,31 +173,31 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(5)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewNotFound", - "uri": "record(6)", + "uri": "record(7)", }, }, ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(5)", + "uri": "record(6)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(5)", - "uri": "record(6)", + "cid": "cids(6)", + "uri": "record(7)", }, }, "facets": Array [ @@ -170,11 +221,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label", }, ], @@ -185,15 +236,15 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(4)", - "uri": "record(5)", + "cid": "cids(5)", + "uri": "record(6)", }, }, "text": "yoohoo label_me", }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, }, @@ -204,7 +255,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -241,16 +309,16 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(5)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewNotFound", - "uri": "record(6)", + "uri": "record(7)", }, }, "indexedAt": "1970-01-01T00:00:00.000Z", @@ -262,8 +330,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(5)", - "uri": "record(6)", + "cid": "cids(6)", + "uri": "record(7)", }, }, "facets": Array [ @@ -284,7 +352,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(5)", + "uri": "record(6)", "viewer": Object {}, }, }, @@ -304,11 +372,11 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -319,7 +387,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(7)", + "uri": "record(8)", "viewer": Object {}, }, }, @@ -330,24 +398,58 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(8)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(9)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(8)", + "uri": "record(9)", "viewer": Object {}, }, }, @@ -363,7 +465,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -399,7 +518,7 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, @@ -413,13 +532,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -428,8 +564,8 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, "root": Object { "cid": "cids(0)", @@ -440,7 +576,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, }, @@ -452,12 +588,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(7)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -478,7 +614,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "reply": Object { @@ -489,7 +625,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -516,7 +669,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -545,28 +715,45 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(5)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewNotFound", - "uri": "record(8)", + "uri": "record(9)", }, }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(4)", + "cid": "cids(5)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(7)", + "uri": "record(8)", "val": "test-label", }, ], @@ -577,15 +764,15 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(5)", - "uri": "record(8)", + "cid": "cids(6)", + "uri": "record(9)", }, }, "text": "yoohoo label_me", }, "replyCount": 0, "repostCount": 0, - "uri": "record(7)", + "uri": "record(8)", "viewer": Object {}, }, }, @@ -596,15 +783,32 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(8)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(13)", + "val": "self-label-a", + }, + Object { + "cid": "cids(8)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(13)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", + "followedBy": "record(12)", + "following": "record(11)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -615,7 +819,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(9)", + "uri": "record(10)", "viewer": Object {}, }, }, @@ -626,7 +830,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -663,11 +884,11 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -678,7 +899,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(12)", + "uri": "record(14)", "viewer": Object {}, }, }, @@ -690,12 +911,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(7)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(10)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -721,28 +942,45 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(8)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(13)", + "val": "self-label-a", + }, + Object { + "cid": "cids(8)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(13)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", + "followedBy": "record(12)", + "following": "record(11)", "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(13)", "embeds": Array [], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(11)", + "cid": "cids(13)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(14)", + "uri": "record(16)", "val": "kind", }, ], - "uri": "record(14)", + "uri": "record(16)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -758,11 +996,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(8)", + "cid": "cids(10)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(13)", + "uri": "record(15)", "val": "kind", }, ], @@ -781,7 +1019,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(9)", + "$link": "cids(11)", }, "size": 4114, }, @@ -792,7 +1030,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(10)", + "$link": "cids(12)", }, "size": 12736, }, @@ -801,8 +1039,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(11)", - "uri": "record(14)", + "cid": "cids(13)", + "uri": "record(16)", }, }, }, @@ -810,9 +1048,9 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(13)", + "uri": "record(15)", "viewer": Object { - "like": "record(15)", + "like": "record(17)", }, }, }, @@ -823,23 +1061,40 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(8)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(13)", + "val": "self-label-a", + }, + Object { + "cid": "cids(8)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(13)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(11)", - "following": "record(10)", + "followedBy": "record(12)", + "following": "record(11)", "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(11)", + "cid": "cids(13)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(14)", + "uri": "record(16)", "val": "kind", }, ], @@ -855,7 +1110,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(14)", + "uri": "record(16)", "viewer": Object {}, }, }, @@ -866,24 +1121,58 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(12)", + "cid": "cids(14)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(14)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(18)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(16)", + "uri": "record(18)", "viewer": Object {}, }, }, @@ -899,7 +1188,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -935,7 +1241,7 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, @@ -958,11 +1264,11 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -973,12 +1279,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -1005,27 +1311,44 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "kind", }, ], - "uri": "record(6)", + "uri": "record(7)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1042,15 +1365,15 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(3)", + "uri": "record(4)", "val": "kind", }, ], - "uri": "record(3)", + "uri": "record(4)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1065,7 +1388,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -1076,7 +1399,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 12736, }, @@ -1085,8 +1408,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(5)", - "uri": "record(6)", + "cid": "cids(6)", + "uri": "record(7)", }, }, }, @@ -1103,8 +1426,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "facets": Array [ @@ -1125,7 +1448,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, "reason": Object { @@ -1136,8 +1459,8 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, @@ -1151,13 +1474,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1166,8 +1506,8 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(7)", - "uri": "record(10)", + "cid": "cids(9)", + "uri": "record(12)", }, "root": Object { "cid": "cids(0)", @@ -1178,7 +1518,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(9)", + "uri": "record(11)", "viewer": Object {}, }, "reply": Object { @@ -1189,15 +1529,32 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -1211,19 +1568,19 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(10)", + "uri": "record(12)", "val": "test-label", }, Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(10)", + "uri": "record(12)", "val": "test-label-2", }, ], @@ -1240,7 +1597,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -1261,7 +1618,7 @@ Array [ }, "replyCount": 1, "repostCount": 0, - "uri": "record(10)", + "uri": "record(12)", "viewer": Object {}, }, "root": Object { @@ -1271,7 +1628,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1301,12 +1675,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1327,7 +1701,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(11)", + "uri": "record(13)", "viewer": Object {}, }, "reply": Object { @@ -1338,7 +1712,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1365,7 +1756,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1394,15 +1802,32 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -1416,19 +1841,19 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(10)", + "uri": "record(12)", "val": "test-label", }, Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(10)", + "uri": "record(12)", "val": "test-label-2", }, ], @@ -1445,7 +1870,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -1466,7 +1891,7 @@ Array [ }, "replyCount": 1, "repostCount": 0, - "uri": "record(10)", + "uri": "record(12)", "viewer": Object {}, }, "reply": Object { @@ -1477,7 +1902,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1504,7 +1946,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1533,13 +1992,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(11)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -1558,11 +2034,11 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", @@ -1574,24 +2050,24 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(3)", + "uri": "record(4)", "val": "kind", }, ], - "uri": "record(3)", + "uri": "record(4)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1606,7 +2082,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -1617,7 +2093,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 12736, }, @@ -1626,8 +2102,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(5)", - "uri": "record(6)", + "cid": "cids(6)", + "uri": "record(7)", }, }, }, @@ -1638,15 +2114,15 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(2)", + "uri": "record(3)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "facets": Array [ @@ -1670,11 +2146,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(9)", + "cid": "cids(11)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(12)", + "uri": "record(14)", "val": "test-label", }, ], @@ -1685,15 +2161,15 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(1)", - "uri": "record(2)", + "cid": "cids(2)", + "uri": "record(3)", }, }, "text": "yoohoo label_me", }, "replyCount": 0, "repostCount": 0, - "uri": "record(12)", + "uri": "record(14)", "viewer": Object {}, }, }, @@ -1704,15 +2180,32 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1723,7 +2216,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(13)", + "uri": "record(15)", "viewer": Object {}, }, }, @@ -1734,7 +2227,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -1771,11 +2281,11 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -1786,12 +2296,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -1818,27 +2328,44 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "kind", }, ], - "uri": "record(6)", + "uri": "record(7)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1855,15 +2382,15 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(3)", + "uri": "record(4)", "val": "kind", }, ], - "uri": "record(3)", + "uri": "record(4)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1878,7 +2405,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -1889,7 +2416,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 12736, }, @@ -1898,8 +2425,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(5)", - "uri": "record(6)", + "cid": "cids(6)", + "uri": "record(7)", }, }, }, @@ -1916,8 +2443,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "facets": Array [ @@ -1938,7 +2465,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, }, @@ -1958,11 +2485,11 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1973,7 +2500,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(14)", + "uri": "record(16)", "viewer": Object {}, }, }, @@ -1985,12 +2512,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -2016,28 +2543,45 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "embeds": Array [], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "kind", }, ], - "uri": "record(6)", + "uri": "record(7)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2053,11 +2597,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(3)", + "uri": "record(4)", "val": "kind", }, ], @@ -2076,7 +2620,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(4)", }, "size": 4114, }, @@ -2087,7 +2631,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 12736, }, @@ -2096,8 +2640,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(5)", - "uri": "record(6)", + "cid": "cids(6)", + "uri": "record(7)", }, }, }, @@ -2105,9 +2649,9 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object { - "like": "record(15)", + "like": "record(17)", }, }, }, @@ -2118,23 +2662,40 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(10)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(8)", - "following": "record(7)", + "followedBy": "record(9)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "kind", }, ], @@ -2150,7 +2711,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(6)", + "uri": "record(7)", "viewer": Object {}, }, }, @@ -2161,24 +2722,58 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(12)", + "cid": "cids(14)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(14)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(18)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(16)", + "uri": "record(18)", "viewer": Object {}, }, }, @@ -2249,7 +2844,24 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -2392,15 +3004,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -2409,19 +3038,19 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(7)", - "uri": "record(9)", + "cid": "cids(9)", + "uri": "record(11)", }, "root": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(10)", }, }, "text": "thanks bob", }, "replyCount": 0, "repostCount": 0, - "uri": "record(5)", + "uri": "record(6)", "viewer": Object {}, }, "reply": Object { @@ -2432,13 +3061,30 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -2452,19 +3098,19 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(11)", "val": "test-label", }, Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(11)", "val": "test-label-2", }, ], @@ -2490,19 +3136,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(10)", }, "root": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(10)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(9)", + "uri": "record(11)", "viewer": Object {}, }, "root": Object { @@ -2512,15 +3158,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -2531,9 +3194,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object { - "like": "record(10)", + "like": "record(12)", }, }, }, @@ -2550,7 +3213,7 @@ Array [ "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -2559,19 +3222,19 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(10)", }, "root": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(10)", }, }, "text": "of course", }, "replyCount": 0, "repostCount": 0, - "uri": "record(11)", + "uri": "record(13)", "viewer": Object {}, }, "reply": Object { @@ -2582,15 +3245,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -2601,9 +3281,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object { - "like": "record(10)", + "like": "record(12)", }, }, "root": Object { @@ -2613,15 +3293,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -2632,9 +3329,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object { - "like": "record(10)", + "like": "record(12)", }, }, }, @@ -2646,13 +3343,30 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -2666,19 +3380,19 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(11)", "val": "test-label", }, Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(11)", "val": "test-label-2", }, ], @@ -2704,19 +3418,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(10)", }, "root": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(10)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(9)", + "uri": "record(11)", "viewer": Object {}, }, "reply": Object { @@ -2727,15 +3441,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -2746,9 +3477,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object { - "like": "record(10)", + "like": "record(12)", }, }, "root": Object { @@ -2758,15 +3489,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -2777,9 +3525,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object { - "like": "record(10)", + "like": "record(12)", }, }, }, @@ -2791,15 +3539,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(11)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -2929,11 +3694,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(9)", + "cid": "cids(11)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(12)", + "uri": "record(14)", "val": "test-label", }, ], @@ -2952,9 +3717,9 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(12)", + "uri": "record(14)", "viewer": Object { - "like": "record(13)", + "like": "record(15)", }, }, }, @@ -2965,13 +3730,30 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -2982,7 +3764,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(14)", + "uri": "record(16)", "viewer": Object {}, }, }, @@ -2993,15 +3775,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -3012,9 +3811,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object { - "like": "record(10)", + "like": "record(12)", }, }, }, @@ -3056,7 +3855,24 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -3145,7 +3961,7 @@ Array [ "repostCount": 0, "uri": "record(2)", "viewer": Object { - "like": "record(15)", + "like": "record(17)", }, }, }, @@ -3156,7 +3972,24 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(5)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -3197,26 +4030,60 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(13)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(18)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(16)", + "uri": "record(18)", "viewer": Object {}, }, }, @@ -3285,7 +4152,24 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(4)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(4)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(3)", @@ -3406,7 +4290,7 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { - "repost": "record(4)", + "repost": "record(5)", }, }, "reason": Object { @@ -3430,15 +4314,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -3447,19 +4348,19 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(7)", - "uri": "record(9)", + "cid": "cids(9)", + "uri": "record(11)", }, "root": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(10)", }, }, "text": "thanks bob", }, "replyCount": 0, "repostCount": 0, - "uri": "record(5)", + "uri": "record(6)", "viewer": Object {}, }, "reply": Object { @@ -3470,14 +4371,31 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(4)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(4)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(3)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -3491,19 +4409,19 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(11)", "val": "test-label", }, Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(11)", "val": "test-label-2", }, ], @@ -3529,19 +4447,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(10)", }, "root": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(10)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(9)", + "uri": "record(11)", "viewer": Object {}, }, "root": Object { @@ -3551,15 +4469,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -3570,9 +4505,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object { - "like": "record(10)", + "like": "record(12)", }, }, }, @@ -3588,7 +4523,7 @@ Array [ "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -3597,19 +4532,19 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(10)", }, "root": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(10)", }, }, "text": "of course", }, "replyCount": 0, "repostCount": 0, - "uri": "record(11)", + "uri": "record(13)", "viewer": Object {}, }, "reply": Object { @@ -3620,15 +4555,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -3639,9 +4591,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object { - "like": "record(10)", + "like": "record(12)", }, }, "root": Object { @@ -3651,15 +4603,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -3670,9 +4639,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object { - "like": "record(10)", + "like": "record(12)", }, }, }, @@ -3684,15 +4653,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(11)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -3820,11 +4806,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(9)", + "cid": "cids(11)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(12)", + "uri": "record(14)", "val": "test-label", }, ], @@ -3843,9 +4829,9 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(12)", + "uri": "record(14)", "viewer": Object { - "like": "record(13)", + "like": "record(15)", }, }, }, @@ -3856,15 +4842,32 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -3875,9 +4878,9 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object { - "like": "record(10)", + "like": "record(12)", }, }, }, @@ -3918,7 +4921,24 @@ Array [ "did": "user(3)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(4)", + "val": "self-label-a", + }, + Object { + "cid": "cids(5)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(3)", + "uri": "record(4)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(3)", @@ -4017,26 +5037,60 @@ Array [ "did": "user(1)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(7)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(7)", - "following": "record(6)", + "followedBy": "record(8)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(12)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(16)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(14)", + "uri": "record(16)", "viewer": Object {}, }, }, @@ -4052,7 +5106,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -4072,8 +5143,8 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", - "repost": "record(2)", + "like": "record(4)", + "repost": "record(3)", }, }, "reason": Object { @@ -4105,14 +5176,31 @@ Array [ "did": "user(2)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "following": "record(5)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -4126,19 +5214,19 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(1)", + "cid": "cids(2)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label", }, Object { - "cid": "cids(1)", + "cid": "cids(2)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(5)", "val": "test-label-2", }, ], @@ -4155,7 +5243,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(2)", + "$link": "cids(4)", }, "size": 4114, }, @@ -4176,7 +5264,7 @@ Array [ }, "replyCount": 1, "repostCount": 0, - "uri": "record(4)", + "uri": "record(5)", "viewer": Object {}, }, "reply": Object { @@ -4187,7 +5275,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -4207,8 +5312,8 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", - "repost": "record(2)", + "like": "record(4)", + "repost": "record(3)", }, }, "root": Object { @@ -4218,7 +5323,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "followedBy": "record(1)", @@ -4238,8 +5360,8 @@ Array [ "repostCount": 1, "uri": "record(0)", "viewer": Object { - "like": "record(3)", - "repost": "record(2)", + "like": "record(4)", + "repost": "record(3)", }, }, }, @@ -4251,14 +5373,31 @@ Array [ "did": "user(2)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "following": "record(5)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(5)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -4269,7 +5408,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(6)", + "uri": "record(8)", "viewer": Object {}, }, }, @@ -4292,7 +5431,7 @@ Array [ "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(6)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -4306,7 +5445,7 @@ Array [ "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(7)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -4333,26 +5472,43 @@ Array [ "did": "user(2)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "following": "record(5)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(11)", "val": "kind", }, ], - "uri": "record(9)", + "uri": "record(11)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -4369,15 +5525,15 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(5)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(8)", + "uri": "record(10)", "val": "kind", }, ], - "uri": "record(8)", + "uri": "record(10)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -4392,7 +5548,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(2)", + "$link": "cids(4)", }, "size": 4114, }, @@ -4403,7 +5559,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(6)", + "$link": "cids(8)", }, "size": 12736, }, @@ -4412,8 +5568,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(7)", - "uri": "record(9)", + "cid": "cids(9)", + "uri": "record(11)", }, }, }, @@ -4430,8 +5586,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(5)", - "uri": "record(8)", + "cid": "cids(7)", + "uri": "record(10)", }, }, "facets": Array [ @@ -4452,7 +5608,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(7)", + "uri": "record(9)", "viewer": Object {}, }, }, @@ -4475,7 +5631,7 @@ Array [ "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -4486,7 +5642,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(10)", + "uri": "record(12)", "viewer": Object {}, }, }, @@ -4497,22 +5653,39 @@ Array [ "did": "user(2)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-a", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "following": "record(5)", + "following": "record(6)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(11)", "val": "kind", }, ], @@ -4528,7 +5701,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(9)", + "uri": "record(11)", "viewer": Object {}, }, }, @@ -4544,7 +5717,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -4580,7 +5770,7 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, @@ -4594,13 +5784,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(1)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -4609,8 +5816,8 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(3)", + "uri": "record(4)", }, "root": Object { "cid": "cids(0)", @@ -4621,7 +5828,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(2)", + "uri": "record(3)", "viewer": Object {}, }, "reply": Object { @@ -4632,15 +5839,32 @@ Array [ "did": "user(2)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": true, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -4654,19 +5878,19 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(3)", + "uri": "record(4)", "val": "test-label", }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(3)", + "uri": "record(4)", "val": "test-label-2", }, ], @@ -4683,7 +5907,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -4704,7 +5928,7 @@ Array [ }, "replyCount": 1, "repostCount": 0, - "uri": "record(3)", + "uri": "record(4)", "viewer": Object {}, }, "root": Object { @@ -4714,7 +5938,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -4743,13 +5984,30 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(6)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -4768,11 +6026,11 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(7)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", @@ -4784,24 +6042,24 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", + "followedBy": "record(12)", + "following": "record(11)", "muted": true, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(8)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(8)", + "uri": "record(10)", "val": "kind", }, ], - "uri": "record(8)", + "uri": "record(10)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -4816,7 +6074,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -4827,7 +6085,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(7)", + "$link": "cids(9)", }, "size": 12736, }, @@ -4836,8 +6094,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(8)", - "uri": "record(11)", + "cid": "cids(10)", + "uri": "record(13)", }, }, }, @@ -4848,15 +6106,15 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(7)", + "uri": "record(9)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(10)", }, }, "facets": Array [ @@ -4880,11 +6138,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(4)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(8)", "val": "test-label", }, ], @@ -4895,15 +6153,15 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(5)", - "uri": "record(7)", + "cid": "cids(7)", + "uri": "record(9)", }, }, "text": "yoohoo label_me", }, "replyCount": 0, "repostCount": 0, - "uri": "record(6)", + "uri": "record(8)", "viewer": Object {}, }, }, @@ -4914,7 +6172,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -4951,11 +6226,11 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(7)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -4966,12 +6241,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", + "followedBy": "record(12)", + "following": "record(11)", "muted": true, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -4998,27 +6273,44 @@ Array [ "did": "user(2)", "displayName": "bobby", "handle": "bob.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(6)", + "following": "record(5)", "muted": true, }, }, - "cid": "cids(8)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(8)", + "cid": "cids(10)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(11)", + "uri": "record(13)", "val": "kind", }, ], - "uri": "record(11)", + "uri": "record(13)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -5035,15 +6327,15 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(8)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(8)", + "uri": "record(10)", "val": "kind", }, ], - "uri": "record(8)", + "uri": "record(10)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -5058,7 +6350,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(3)", + "$link": "cids(5)", }, "size": 4114, }, @@ -5069,7 +6361,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(7)", + "$link": "cids(9)", }, "size": 12736, }, @@ -5078,8 +6370,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(8)", - "uri": "record(11)", + "cid": "cids(10)", + "uri": "record(13)", }, }, }, @@ -5096,8 +6388,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(6)", - "uri": "record(8)", + "cid": "cids(8)", + "uri": "record(10)", }, }, "facets": Array [ @@ -5118,7 +6410,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(7)", + "uri": "record(9)", "viewer": Object {}, }, }, @@ -5138,11 +6430,11 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(2)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(11)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -5153,7 +6445,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(12)", + "uri": "record(14)", "viewer": Object {}, }, }, @@ -5164,24 +6456,58 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(12)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(15)", + "val": "self-label", + }, + ], "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, "replyCount": 0, "repostCount": 0, - "uri": "record(13)", + "uri": "record(15)", "viewer": Object {}, }, }, diff --git a/packages/pds/tests/views/admin/__snapshots__/get-moderation-action.test.ts.snap b/packages/pds/tests/views/admin/__snapshots__/get-moderation-action.test.ts.snap index 2c36fee7ee9..aedd7a5a7ea 100644 --- a/packages/pds/tests/views/admin/__snapshots__/get-moderation-action.test.ts.snap +++ b/packages/pds/tests/views/admin/__snapshots__/get-moderation-action.test.ts.snap @@ -56,6 +56,17 @@ Object { }, "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", + }, + ], + }, }, ], }, @@ -63,6 +74,14 @@ Object { "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, }, @@ -134,6 +153,17 @@ Object { }, "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", + }, + ], + }, }, ], }, diff --git a/packages/pds/tests/views/admin/__snapshots__/get-moderation-report.test.ts.snap b/packages/pds/tests/views/admin/__snapshots__/get-moderation-report.test.ts.snap index ef546a504d1..70e829d0ab0 100644 --- a/packages/pds/tests/views/admin/__snapshots__/get-moderation-report.test.ts.snap +++ b/packages/pds/tests/views/admin/__snapshots__/get-moderation-report.test.ts.snap @@ -77,6 +77,17 @@ Object { }, "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", + }, + ], + }, }, ], }, @@ -84,6 +95,14 @@ Object { "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, }, @@ -140,6 +159,17 @@ Object { }, "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", + }, + ], + }, }, ], }, diff --git a/packages/pds/tests/views/admin/__snapshots__/get-record.test.ts.snap b/packages/pds/tests/views/admin/__snapshots__/get-record.test.ts.snap index 256c66a4203..c9eda38e356 100644 --- a/packages/pds/tests/views/admin/__snapshots__/get-record.test.ts.snap +++ b/packages/pds/tests/views/admin/__snapshots__/get-record.test.ts.snap @@ -97,6 +97,17 @@ Object { }, "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", + }, + ], + }, }, ], }, @@ -104,6 +115,14 @@ Object { "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, } @@ -206,6 +225,17 @@ Object { }, "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", + }, + ], + }, }, ], }, @@ -213,6 +243,14 @@ Object { "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, } @@ -332,6 +370,17 @@ Object { }, "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", + }, + ], + }, }, ], }, @@ -339,6 +388,14 @@ Object { "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, "text": "hey there", }, } diff --git a/packages/pds/tests/views/admin/__snapshots__/get-repo.test.ts.snap b/packages/pds/tests/views/admin/__snapshots__/get-repo.test.ts.snap index 4e93c13e2c2..7234789c2c1 100644 --- a/packages/pds/tests/views/admin/__snapshots__/get-repo.test.ts.snap +++ b/packages/pds/tests/views/admin/__snapshots__/get-repo.test.ts.snap @@ -86,6 +86,17 @@ Object { }, "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", + }, + ], + }, }, ], } @@ -192,6 +203,17 @@ Object { }, "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", + }, + ], + }, }, ], } diff --git a/packages/pds/tests/views/profile.test.ts b/packages/pds/tests/views/profile.test.ts index b3f5520a8b9..b1a3e5003a9 100644 --- a/packages/pds/tests/views/profile.test.ts +++ b/packages/pds/tests/views/profile.test.ts @@ -43,6 +43,20 @@ describe('pds profile views', () => { expect(forSnapshot(aliceForAlice.data)).toMatchSnapshot() }) + it('reflects self-labels', async () => { + const aliceForBob = await agent.api.app.bsky.actor.getProfile( + { actor: alice }, + { headers: sc.getHeaders(bob) }, + ) + + const labels = aliceForBob.data.labels + ?.filter((label) => label.src === alice) + .map((label) => label.val) + .sort() + + expect(labels).toEqual(['self-label-a', 'self-label-b']) + }) + it("fetches other's profile, with a follow", async () => { const aliceForBob = await agent.api.app.bsky.actor.getProfile( { actor: alice }, diff --git a/packages/pds/tests/views/thread.test.ts b/packages/pds/tests/views/thread.test.ts index 8838ea20e44..88a595a6fc0 100644 --- a/packages/pds/tests/views/thread.test.ts +++ b/packages/pds/tests/views/thread.test.ts @@ -1,5 +1,7 @@ +import assert from 'assert' import AtpAgent, { AppBskyFeedGetPostThread } from '@atproto/api' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' +import { isThreadViewPost } from '@atproto/api/src/client/types/app/bsky/feed/defs' import { Database } from '../../src' import { runTestServer, @@ -165,6 +167,29 @@ describe('pds thread views', () => { expect(forSnapshot(thread3.data.thread)).toMatchSnapshot() }) + it('reflects self-labels', async () => { + const { data: thread } = await agent.api.app.bsky.feed.getPostThread( + { uri: sc.posts[alice][0].ref.uriStr }, + { headers: sc.getHeaders(bob) }, + ) + + assert(isThreadViewPost(thread.thread), 'post does not exist') + const post = thread.thread.post + + const postSelfLabels = post.labels + ?.filter((label) => label.src === alice) + .map((label) => label.val) + + expect(postSelfLabels).toEqual(['self-label']) + + const authorSelfLabels = post.author.labels + ?.filter((label) => label.src === alice) + .map((label) => label.val) + .sort() + + expect(authorSelfLabels).toEqual(['self-label-a', 'self-label-b']) + }) + it('blocks post by actor takedown', async () => { const { data: modAction } = await agent.api.com.atproto.admin.takeModerationAction( diff --git a/packages/pds/tests/views/timeline.test.ts b/packages/pds/tests/views/timeline.test.ts index b55a96e691d..5dd9ba4a893 100644 --- a/packages/pds/tests/views/timeline.test.ts +++ b/packages/pds/tests/views/timeline.test.ts @@ -1,3 +1,4 @@ +import assert from 'assert' import AtpAgent from '@atproto/api' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { FeedViewPost } from '../../src/lexicon/types/app/bsky/feed/defs' @@ -186,6 +187,32 @@ describe('timeline views', () => { expect(results(paginatedAll)).toEqual(results([full.data])) }) + it('reflects self-labels', async () => { + const carolTL = await agent.api.app.bsky.feed.getTimeline( + {}, + { headers: sc.getHeaders(carol) }, + ) + + const alicePost = carolTL.data.feed.find( + ({ post }) => post.uri === sc.posts[alice][0].ref.uriStr, + )?.post + + assert(alicePost, 'post does not exist') + + const postSelfLabels = alicePost.labels + ?.filter((label) => label.src === alice) + .map((label) => label.val) + + expect(postSelfLabels).toEqual(['self-label']) + + const authorSelfLabels = alicePost.author.labels + ?.filter((label) => label.src === alice) + .map((label) => label.val) + .sort() + + expect(authorSelfLabels).toEqual(['self-label-a', 'self-label-b']) + }) + it('blocks posts, reposts, replies by actor takedown', async () => { const actionResults = await Promise.all( [bob, carol].map((did) => From b406a45829595b5acea8c52917cecec9e5c79971 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 9 Aug 2023 16:08:05 -0700 Subject: [PATCH 124/237] @atproto/api@0.6.0 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 7b4cbafa92e..9a1a56f4a7c 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.5.4", + "version": "0.6.0", "main": "src/index.ts", "scripts": { "codegen": "yarn docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From 38a4b25c274c62e86ae240db037731eebf5b05ec Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Wed, 9 Aug 2023 19:11:15 -0400 Subject: [PATCH 125/237] v0.2.3 --- packages/dev-env/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index 830cf630bf7..179d1990004 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/dev-env", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "bin": "dist/bin.js", "license": "MIT", From 3f0e1b25633a67e57c9b92dfba158e0e7b6aba58 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 10 Aug 2023 13:42:31 -0500 Subject: [PATCH 126/237] Read after write (#1398) * first pass on profiles * quick test * wip * wip * test post thread * record embeds * get author feed profiles * wip timeline * fix get timeline * switch from counter to tid * tidy into a service * quick tid test * pr feedback * clock -> rev * update image formatting * disable migration & build branch * add recent posts to getAuthorFeed & handle post thread not found errors * refactor for lag header * tidy * rm collections check * tidy test * pr feedback * fix small bug * build branch * get migrations into system * enable migrations --- .../types/app/bsky/feed/getAuthorFeed.ts | 4 +- .../src/client/types/app/bsky/feed/getFeed.ts | 2 +- .../types/app/bsky/feed/getFeedSkeleton.ts | 2 +- .../types/app/bsky/feed/getPostThread.ts | 2 +- .../app/bsky/unspecced/getTimelineSkeleton.ts | 2 +- .../types/com/atproto/admin/getRecord.ts | 2 +- .../client/types/com/atproto/admin/getRepo.ts | 2 +- .../types/com/atproto/admin/rebaseRepo.ts | 4 +- .../com/atproto/admin/takeModerationAction.ts | 2 +- .../types/com/atproto/repo/applyWrites.ts | 2 +- .../types/com/atproto/repo/createRecord.ts | 2 +- .../types/com/atproto/repo/deleteRecord.ts | 2 +- .../types/com/atproto/repo/putRecord.ts | 2 +- .../types/com/atproto/repo/rebaseRepo.ts | 4 +- .../types/com/atproto/server/createAccount.ts | 14 +- .../com/atproto/server/createAppPassword.ts | 2 +- .../types/com/atproto/server/createSession.ts | 2 +- .../types/com/atproto/server/deleteAccount.ts | 4 +- .../atproto/server/getAccountInviteCodes.ts | 2 +- .../com/atproto/server/listAppPasswords.ts | 2 +- .../com/atproto/server/refreshSession.ts | 2 +- .../types/com/atproto/server/resetPassword.ts | 4 +- .../client/types/com/atproto/sync/getHead.ts | 2 +- .../bsky/src/api/app/bsky/actor/getProfile.ts | 9 +- .../src/api/app/bsky/actor/getProfiles.ts | 9 +- .../src/api/app/bsky/feed/getAuthorFeed.ts | 11 +- .../src/api/app/bsky/feed/getPostThread.ts | 11 +- .../bsky/src/api/app/bsky/feed/getTimeline.ts | 9 +- packages/bsky/src/api/util.ts | 7 + .../20230808T172902639Z-repo-rev.ts | 12 + packages/bsky/src/db/migrations/index.ts | 1 + packages/bsky/src/db/tables/actor-sync.ts | 1 + packages/bsky/src/services/actor/index.ts | 10 + packages/bsky/src/services/indexing/index.ts | 2 + packages/common-web/src/tid.ts | 12 +- packages/common-web/tests/tid.test.ts | 6 + packages/dev-env/src/pds.ts | 1 + packages/lex-cli/src/codegen/client.ts | 2 +- .../app-view/api/app/bsky/actor/getProfile.ts | 22 ++ .../api/app/bsky/actor/getProfiles.ts | 34 +- .../api/app/bsky/feed/getAuthorFeed.ts | 41 ++ .../api/app/bsky/feed/getPostThread.ts | 203 +++++++++- .../app-view/api/app/bsky/feed/getTimeline.ts | 22 +- .../api/app/bsky/util/read-after-write.ts | 80 ++++ packages/pds/src/config.ts | 10 + packages/pds/src/context.ts | 15 +- .../20230808T172813122Z-repo-rev.ts | 15 + packages/pds/src/db/migrations/index.ts | 1 + packages/pds/src/db/tables/record.ts | 1 + packages/pds/src/index.ts | 9 + packages/pds/src/services/index.ts | 15 + packages/pds/src/services/local/index.ts | 363 ++++++++++++++++++ packages/pds/src/services/record/index.ts | 10 +- packages/pds/src/services/repo/index.ts | 5 +- .../tests/proxied/read-after-write.test.ts | 178 +++++++++ packages/pds/tests/seeds/client.ts | 19 + packages/repo/src/repo.ts | 4 + packages/repo/src/types.ts | 3 + packages/xrpc/src/client.ts | 7 +- packages/xrpc/src/types.ts | 3 + 60 files changed, 1153 insertions(+), 78 deletions(-) create mode 100644 packages/bsky/src/api/util.ts create mode 100644 packages/bsky/src/db/migrations/20230808T172902639Z-repo-rev.ts create mode 100644 packages/pds/src/app-view/api/app/bsky/util/read-after-write.ts create mode 100644 packages/pds/src/db/migrations/20230808T172813122Z-repo-rev.ts create mode 100644 packages/pds/src/services/local/index.ts create mode 100644 packages/pds/tests/proxied/read-after-write.test.ts diff --git a/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts b/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts index a2107220efa..3f3abc9933f 100644 --- a/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/api/src/client/types/app/bsky/feed/getAuthorFeed.ts @@ -39,13 +39,13 @@ export interface Response { export class BlockedActorError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } export class BlockedByActorError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/app/bsky/feed/getFeed.ts b/packages/api/src/client/types/app/bsky/feed/getFeed.ts index dfc05bd2481..65ede1fd2a4 100644 --- a/packages/api/src/client/types/app/bsky/feed/getFeed.ts +++ b/packages/api/src/client/types/app/bsky/feed/getFeed.ts @@ -34,7 +34,7 @@ export interface Response { export class UnknownFeedError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/app/bsky/feed/getFeedSkeleton.ts b/packages/api/src/client/types/app/bsky/feed/getFeedSkeleton.ts index 434010b1c3f..0aa325d7fec 100644 --- a/packages/api/src/client/types/app/bsky/feed/getFeedSkeleton.ts +++ b/packages/api/src/client/types/app/bsky/feed/getFeedSkeleton.ts @@ -34,7 +34,7 @@ export interface Response { export class UnknownFeedError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/app/bsky/feed/getPostThread.ts b/packages/api/src/client/types/app/bsky/feed/getPostThread.ts index 44ab27fcefe..d3865db9ee2 100644 --- a/packages/api/src/client/types/app/bsky/feed/getPostThread.ts +++ b/packages/api/src/client/types/app/bsky/feed/getPostThread.ts @@ -37,7 +37,7 @@ export interface Response { export class NotFoundError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/api/src/client/types/app/bsky/unspecced/getTimelineSkeleton.ts index de9ba9c0ae9..91acf9d5e3d 100644 --- a/packages/api/src/client/types/app/bsky/unspecced/getTimelineSkeleton.ts +++ b/packages/api/src/client/types/app/bsky/unspecced/getTimelineSkeleton.ts @@ -33,7 +33,7 @@ export interface Response { export class UnknownFeedError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/admin/getRecord.ts b/packages/api/src/client/types/com/atproto/admin/getRecord.ts index 201e1934c49..453e94c39d7 100644 --- a/packages/api/src/client/types/com/atproto/admin/getRecord.ts +++ b/packages/api/src/client/types/com/atproto/admin/getRecord.ts @@ -28,7 +28,7 @@ export interface Response { export class RecordNotFoundError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/admin/getRepo.ts b/packages/api/src/client/types/com/atproto/admin/getRepo.ts index f880dd86ac0..5391bada281 100644 --- a/packages/api/src/client/types/com/atproto/admin/getRepo.ts +++ b/packages/api/src/client/types/com/atproto/admin/getRepo.ts @@ -27,7 +27,7 @@ export interface Response { export class RepoNotFoundError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/admin/rebaseRepo.ts b/packages/api/src/client/types/com/atproto/admin/rebaseRepo.ts index fc91da19bf7..8b6c397c4d6 100644 --- a/packages/api/src/client/types/com/atproto/admin/rebaseRepo.ts +++ b/packages/api/src/client/types/com/atproto/admin/rebaseRepo.ts @@ -30,13 +30,13 @@ export interface Response { export class InvalidSwapError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } export class ConcurrentWritesError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/admin/takeModerationAction.ts b/packages/api/src/client/types/com/atproto/admin/takeModerationAction.ts index c1f0de18433..4e769609fde 100644 --- a/packages/api/src/client/types/com/atproto/admin/takeModerationAction.ts +++ b/packages/api/src/client/types/com/atproto/admin/takeModerationAction.ts @@ -45,7 +45,7 @@ export interface Response { export class SubjectHasActionError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/repo/applyWrites.ts b/packages/api/src/client/types/com/atproto/repo/applyWrites.ts index a56b082d331..b25c3716e1b 100644 --- a/packages/api/src/client/types/com/atproto/repo/applyWrites.ts +++ b/packages/api/src/client/types/com/atproto/repo/applyWrites.ts @@ -32,7 +32,7 @@ export interface Response { export class InvalidSwapError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/repo/createRecord.ts b/packages/api/src/client/types/com/atproto/repo/createRecord.ts index 84d1d49a863..27662fc4929 100644 --- a/packages/api/src/client/types/com/atproto/repo/createRecord.ts +++ b/packages/api/src/client/types/com/atproto/repo/createRecord.ts @@ -45,7 +45,7 @@ export interface Response { export class InvalidSwapError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/repo/deleteRecord.ts b/packages/api/src/client/types/com/atproto/repo/deleteRecord.ts index 5363a974da0..4bbe1fd2341 100644 --- a/packages/api/src/client/types/com/atproto/repo/deleteRecord.ts +++ b/packages/api/src/client/types/com/atproto/repo/deleteRecord.ts @@ -36,7 +36,7 @@ export interface Response { export class InvalidSwapError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/repo/putRecord.ts b/packages/api/src/client/types/com/atproto/repo/putRecord.ts index 62cdb7b63ca..7fbf2630b81 100644 --- a/packages/api/src/client/types/com/atproto/repo/putRecord.ts +++ b/packages/api/src/client/types/com/atproto/repo/putRecord.ts @@ -47,7 +47,7 @@ export interface Response { export class InvalidSwapError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/repo/rebaseRepo.ts b/packages/api/src/client/types/com/atproto/repo/rebaseRepo.ts index fc91da19bf7..8b6c397c4d6 100644 --- a/packages/api/src/client/types/com/atproto/repo/rebaseRepo.ts +++ b/packages/api/src/client/types/com/atproto/repo/rebaseRepo.ts @@ -30,13 +30,13 @@ export interface Response { export class InvalidSwapError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } export class ConcurrentWritesError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/server/createAccount.ts b/packages/api/src/client/types/com/atproto/server/createAccount.ts index 52ee12599c7..3eeaab250b4 100644 --- a/packages/api/src/client/types/com/atproto/server/createAccount.ts +++ b/packages/api/src/client/types/com/atproto/server/createAccount.ts @@ -41,43 +41,43 @@ export interface Response { export class InvalidHandleError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } export class InvalidPasswordError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } export class InvalidInviteCodeError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } export class HandleNotAvailableError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } export class UnsupportedDomainError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } export class UnresolvableDidError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } export class IncompatibleDidDocError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/server/createAppPassword.ts b/packages/api/src/client/types/com/atproto/server/createAppPassword.ts index 2dc001146b0..d6e9ce3ddf5 100644 --- a/packages/api/src/client/types/com/atproto/server/createAppPassword.ts +++ b/packages/api/src/client/types/com/atproto/server/createAppPassword.ts @@ -30,7 +30,7 @@ export interface Response { export class AccountTakedownError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/server/createSession.ts b/packages/api/src/client/types/com/atproto/server/createSession.ts index 4df0341dd20..d86f2aef1d4 100644 --- a/packages/api/src/client/types/com/atproto/server/createSession.ts +++ b/packages/api/src/client/types/com/atproto/server/createSession.ts @@ -39,7 +39,7 @@ export interface Response { export class AccountTakedownError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/server/deleteAccount.ts b/packages/api/src/client/types/com/atproto/server/deleteAccount.ts index 0b664474552..403de18a345 100644 --- a/packages/api/src/client/types/com/atproto/server/deleteAccount.ts +++ b/packages/api/src/client/types/com/atproto/server/deleteAccount.ts @@ -29,13 +29,13 @@ export interface Response { export class ExpiredTokenError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } export class InvalidTokenError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/server/getAccountInviteCodes.ts b/packages/api/src/client/types/com/atproto/server/getAccountInviteCodes.ts index 451d7800f21..d019ed3fa23 100644 --- a/packages/api/src/client/types/com/atproto/server/getAccountInviteCodes.ts +++ b/packages/api/src/client/types/com/atproto/server/getAccountInviteCodes.ts @@ -32,7 +32,7 @@ export interface Response { export class DuplicateCreateError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/server/listAppPasswords.ts b/packages/api/src/client/types/com/atproto/server/listAppPasswords.ts index fbdecbb1918..ee5f1c12c82 100644 --- a/packages/api/src/client/types/com/atproto/server/listAppPasswords.ts +++ b/packages/api/src/client/types/com/atproto/server/listAppPasswords.ts @@ -28,7 +28,7 @@ export interface Response { export class AccountTakedownError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/server/refreshSession.ts b/packages/api/src/client/types/com/atproto/server/refreshSession.ts index ae566ce88ec..5b531b19e9d 100644 --- a/packages/api/src/client/types/com/atproto/server/refreshSession.ts +++ b/packages/api/src/client/types/com/atproto/server/refreshSession.ts @@ -32,7 +32,7 @@ export interface Response { export class AccountTakedownError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/server/resetPassword.ts b/packages/api/src/client/types/com/atproto/server/resetPassword.ts index c7b1fbb93d2..f0a3a68e50b 100644 --- a/packages/api/src/client/types/com/atproto/server/resetPassword.ts +++ b/packages/api/src/client/types/com/atproto/server/resetPassword.ts @@ -28,13 +28,13 @@ export interface Response { export class ExpiredTokenError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } export class InvalidTokenError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/api/src/client/types/com/atproto/sync/getHead.ts b/packages/api/src/client/types/com/atproto/sync/getHead.ts index 717ef7b5a8c..c71de962b01 100644 --- a/packages/api/src/client/types/com/atproto/sync/getHead.ts +++ b/packages/api/src/client/types/com/atproto/sync/getHead.ts @@ -31,7 +31,7 @@ export interface Response { export class HeadNotFoundError extends XRPCError { constructor(src: XRPCError) { - super(src.status, src.error, src.message) + super(src.status, src.error, src.message, src.headers) } } diff --git a/packages/bsky/src/api/app/bsky/actor/getProfile.ts b/packages/bsky/src/api/app/bsky/actor/getProfile.ts index e1db6abfd1e..21239a2eb86 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfile.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfile.ts @@ -2,17 +2,22 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import { softDeleted } from '../../../../db/util' import AppContext from '../../../../context' +import { setRepoRev } from '../../../util' export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.getProfile({ auth: ctx.authOptionalVerifier, - handler: async ({ auth, params }) => { + handler: async ({ auth, params, res }) => { const { actor } = params const requester = auth.credentials.did const { db, services } = ctx const actorService = services.actor(db) - const actorRes = await actorService.getActor(actor, true) + const [actorRes, repoRev] = await Promise.all([ + actorService.getActor(actor, true), + actorService.getRepoRev(requester), + ]) + setRepoRev(res, repoRev) if (!actorRes) { throw new InvalidRequestError('Profile not found') diff --git a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts index 3b97a03475b..fb3f1fb518c 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts @@ -1,16 +1,21 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { setRepoRev } from '../../../util' export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.getProfiles({ auth: ctx.authOptionalVerifier, - handler: async ({ auth, params }) => { + handler: async ({ auth, params, res }) => { const { actors } = params const requester = auth.credentials.did const { db, services } = ctx const actorService = services.actor(db) - const actorsRes = await actorService.getActors(actors) + const [actorsRes, repoRev] = await Promise.all([ + actorService.getActors(actors), + actorService.getRepoRev(requester), + ]) + setRepoRev(res, repoRev) return { encoding: 'application/json', diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 42d1af5cd36..b71cafbec02 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -3,11 +3,12 @@ import { FeedKeyset } from '../util/feed' import { paginate } from '../../../../db/pagination' import AppContext from '../../../../context' import { InvalidRequestError } from '@atproto/xrpc-server' +import { setRepoRev } from '../../../util' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getAuthorFeed({ auth: ctx.authOptionalVerifier, - handler: async ({ params, auth }) => { + handler: async ({ params, auth, res }) => { const { actor, limit, cursor, filter } = params const viewer = auth.credentials.did const db = ctx.db.db @@ -29,6 +30,7 @@ export default function (server: Server, ctx: AppContext) { } } + const actorService = ctx.services.actor(ctx.db) const feedService = ctx.services.feed(ctx.db) const graphService = ctx.services.graph(ctx.db) @@ -86,7 +88,12 @@ export default function (server: Server, ctx: AppContext) { keyset, }) - const feedItems = await feedItemsQb.execute() + const [feedItems, repoRev] = await Promise.all([ + feedItemsQb.execute(), + actorService.getRepoRev(viewer), + ]) + setRepoRev(res, repoRev) + const feed = await feedService.hydrateFeed(feedItems, viewer) return { diff --git a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts index 2448cc131a4..ca7e0bbbcdc 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts @@ -19,6 +19,7 @@ import { getAncestorsAndSelfQb, getDescendentsQb, } from '../../../../services/util/post' +import { setRepoRev } from '../../../util' export type PostThread = { post: FeedRow @@ -29,14 +30,20 @@ export type PostThread = { export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getPostThread({ auth: ctx.authOptionalVerifier, - handler: async ({ params, auth }) => { + handler: async ({ params, auth, res }) => { const { uri, depth, parentHeight } = params const requester = auth.credentials.did + const actorService = ctx.services.actor(ctx.db) const feedService = ctx.services.feed(ctx.db) const labelService = ctx.services.label(ctx.db) - const threadData = await getThreadData(ctx, uri, depth, parentHeight) + const [threadData, repoRev] = await Promise.all([ + getThreadData(ctx, uri, depth, parentHeight), + actorService.getRepoRev(requester), + ]) + setRepoRev(res, repoRev) + if (!threadData) { throw new InvalidRequestError(`Post not found: ${uri}`, 'NotFound') } diff --git a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts index 80598220ffb..42713690829 100644 --- a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts +++ b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts @@ -5,11 +5,12 @@ import { paginate } from '../../../../db/pagination' import AppContext from '../../../../context' import Database from '../../../../db' import { SkeletonFeedPost } from '../../../../lexicon/types/app/bsky/feed/defs' +import { setRepoRev } from '../../../util' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getTimeline({ auth: ctx.authVerifier, - handler: async ({ params, auth }) => { + handler: async ({ params, auth, res }) => { const { algorithm, limit, cursor } = params const viewer = auth.credentials.did @@ -17,7 +18,11 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`) } - const skeleton = await getTimelineSkeleton(ctx.db, viewer, limit, cursor) + const [skeleton, repoRev] = await Promise.all([ + getTimelineSkeleton(ctx.db, viewer, limit, cursor), + ctx.services.actor(ctx.db).getRepoRev(viewer), + ]) + setRepoRev(res, repoRev) const feedService = ctx.services.feed(ctx.db) const feedItems = await feedService.cleanFeedSkeleton( diff --git a/packages/bsky/src/api/util.ts b/packages/bsky/src/api/util.ts new file mode 100644 index 00000000000..ef7e51bc95e --- /dev/null +++ b/packages/bsky/src/api/util.ts @@ -0,0 +1,7 @@ +import express from 'express' + +export const setRepoRev = (res: express.Response, rev: string | null) => { + if (rev !== null) { + res.setHeader('Atproto-Repo-Rev', rev) + } +} diff --git a/packages/bsky/src/db/migrations/20230808T172902639Z-repo-rev.ts b/packages/bsky/src/db/migrations/20230808T172902639Z-repo-rev.ts new file mode 100644 index 00000000000..0e0141b073b --- /dev/null +++ b/packages/bsky/src/db/migrations/20230808T172902639Z-repo-rev.ts @@ -0,0 +1,12 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .alterTable('actor_sync') + .addColumn('repoRev', 'varchar') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.alterTable('actor_sync').dropColumn('repoRev').execute() +} diff --git a/packages/bsky/src/db/migrations/index.ts b/packages/bsky/src/db/migrations/index.ts index dd4cfb3c641..5bf06bd5c81 100644 --- a/packages/bsky/src/db/migrations/index.ts +++ b/packages/bsky/src/db/migrations/index.ts @@ -22,3 +22,4 @@ export * as _20230629T220835893Z from './20230629T220835893Z-remove-post-hierarc export * as _20230703T045536691Z from './20230703T045536691Z-feed-and-label-indices' export * as _20230720T164800037Z from './20230720T164800037Z-posts-cursor-idx' export * as _20230807T035309811Z from './20230807T035309811Z-feed-item-delete-invite-for-user-idx' +export * as _20230808T172902639Z from './20230808T172902639Z-repo-rev' diff --git a/packages/bsky/src/db/tables/actor-sync.ts b/packages/bsky/src/db/tables/actor-sync.ts index 0ab0446ee7b..a9e2372ccb2 100644 --- a/packages/bsky/src/db/tables/actor-sync.ts +++ b/packages/bsky/src/db/tables/actor-sync.ts @@ -2,6 +2,7 @@ export interface ActorSync { did: string commitCid: string commitDataCid: string + repoRev: string | null rebaseCount: number tooBigCount: number } diff --git a/packages/bsky/src/services/actor/index.ts b/packages/bsky/src/services/actor/index.ts index 695af2e92bb..ccb86d944e4 100644 --- a/packages/bsky/src/services/actor/index.ts +++ b/packages/bsky/src/services/actor/index.ts @@ -105,6 +105,16 @@ export class ActorService { } return builder } + + async getRepoRev(did: string | null): Promise { + if (did === null) return null + const res = await this.db.db + .selectFrom('actor_sync') + .select('repoRev') + .where('did', '=', did) + .executeTakeFirst() + return res?.repoRev ?? null + } } type ActorResult = Actor diff --git a/packages/bsky/src/services/indexing/index.ts b/packages/bsky/src/services/indexing/index.ts index 25ecb8ccebe..9f73580b5af 100644 --- a/packages/bsky/src/services/indexing/index.ts +++ b/packages/bsky/src/services/indexing/index.ts @@ -225,6 +225,7 @@ export class IndexingService { did: commit.did, commitCid: details.commit.toString(), commitDataCid: commit.data.toString(), + repoRev: commit.rev ?? null, rebaseCount: details.rebase ? 1 : 0, tooBigCount: details.tooBig ? 1 : 0, }) @@ -234,6 +235,7 @@ export class IndexingService { return oc.column('did').doUpdateSet({ commitCid: sql`${excluded('commitCid')}`, commitDataCid: sql`${excluded('commitDataCid')}`, + repoRev: sql`${excluded('repoRev')}`, rebaseCount: sql`${sync('rebaseCount')} + ${excluded('rebaseCount')}`, tooBigCount: sql`${sync('tooBigCount')} + ${excluded('tooBigCount')}`, }) diff --git a/packages/common-web/src/tid.ts b/packages/common-web/src/tid.ts index 21d00b67624..d802956854e 100644 --- a/packages/common-web/src/tid.ts +++ b/packages/common-web/src/tid.ts @@ -21,7 +21,7 @@ export class TID { this.str = noDashes } - static next(): TID { + static next(prev?: TID): TID { // javascript does not have microsecond precision // instead, we append a counter to the timestamp to indicate if multiple timestamps were created within the same millisecond // take max of current time & last timestamp to prevent tids moving backwards if system clock drifts backwards @@ -36,11 +36,15 @@ export class TID { if (clockid === null) { clockid = Math.floor(Math.random() * 32) } - return TID.fromTime(timestamp, clockid) + const tid = TID.fromTime(timestamp, clockid) + if (!prev || tid.newerThan(prev)) { + return tid + } + return TID.fromTime(prev.timestamp() + 1, clockid) } - static nextStr(): string { - return TID.next().toString() + static nextStr(prev?: string): string { + return TID.next(prev ? new TID(prev) : undefined).toString() } static fromTime(timestamp: number, clockid: number): TID { diff --git a/packages/common-web/tests/tid.test.ts b/packages/common-web/tests/tid.test.ts index 00dac05b0ec..cc3cc7c0c8b 100644 --- a/packages/common-web/tests/tid.test.ts +++ b/packages/common-web/tests/tid.test.ts @@ -26,6 +26,12 @@ describe('TIDs', () => { expect(typeof str).toEqual('string') expect(str.length).toEqual(13) }) + + it('returns a next tid larger than a provided prev', () => { + const prev = TID.fromTime((Date.now() + 5000) * 1000, 0).toString() + const str = TID.nextStr(prev) + expect(str > prev).toBe(true) + }) }) describe('newestFirst', () => { diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 88174d0c9a9..091e0a9bea3 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -67,6 +67,7 @@ export class TestPds { feedGenDid: 'did:example:feedGen', dbTxLockNonce: await randomStr(32, 'base32'), bskyAppViewProxy: !!cfg.bskyAppViewEndpoint, + bskyAppViewCdnUrlPattern: 'http://cdn.appview.com/%s/%s/%s', ...cfg, }) diff --git a/packages/lex-cli/src/codegen/client.ts b/packages/lex-cli/src/codegen/client.ts index 91475fb67f3..06998ed4610 100644 --- a/packages/lex-cli/src/codegen/client.ts +++ b/packages/lex-cli/src/codegen/client.ts @@ -607,7 +607,7 @@ function genClientXrpcCommon( .addConstructor({ parameters: [{ name: 'src', type: 'XRPCError' }], }) - .setBodyText(`super(src.status, src.error, src.message)`) + .setBodyText(`super(src.status, src.error, src.message, src.headers)`) customErrors.push({ name: error.name, cls: name }) } diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts b/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts index f84ecb7ef9d..f771bd684c8 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts @@ -2,6 +2,9 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../../lexicon' import { softDeleted } from '../../../../../db/util' import AppContext from '../../../../../context' +import { OutputSchema } from '../../../../../lexicon/types/app/bsky/actor/getProfile' +import { handleReadAfterWrite } from '../util/read-after-write' +import { LocalRecords } from '../../../../../services/local' export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.getProfile({ @@ -13,6 +16,14 @@ export default function (server: Server, ctx: AppContext) { params, await ctx.serviceAuthHeaders(requester), ) + if (res.data.did === requester) { + return await handleReadAfterWrite( + ctx, + requester, + res, + getProfileMunge, + ) + } return { encoding: 'application/json', body: res.data, @@ -49,3 +60,14 @@ export default function (server: Server, ctx: AppContext) { }, }) } + +const getProfileMunge = async ( + ctx: AppContext, + original: OutputSchema, + local: LocalRecords, +): Promise => { + if (!local.profile) return original + return ctx.services + .local(ctx.db) + .updateProfileDetailed(original, local.profile.record) +} diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts b/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts index 78719975c30..e17edce430d 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts @@ -1,5 +1,8 @@ -import { Server } from '../../../../../lexicon' import AppContext from '../../../../../context' +import { Server } from '../../../../../lexicon' +import { OutputSchema } from '../../../../../lexicon/types/app/bsky/actor/getProfiles' +import { LocalRecords } from '../../../../../services/local' +import { handleReadAfterWrite } from '../util/read-after-write' export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.getProfiles({ @@ -11,6 +14,15 @@ export default function (server: Server, ctx: AppContext) { params, await ctx.serviceAuthHeaders(requester), ) + const hasSelf = res.data.profiles.some((prof) => prof.did === requester) + if (hasSelf) { + return await handleReadAfterWrite( + ctx, + requester, + res, + getProfilesMunge, + ) + } return { encoding: 'application/json', body: res.data, @@ -35,3 +47,23 @@ export default function (server: Server, ctx: AppContext) { }, }) } + +const getProfilesMunge = async ( + ctx: AppContext, + original: OutputSchema, + local: LocalRecords, + requester: string, +): Promise => { + const localProf = local.profile + if (!localProf) return original + const profiles = original.profiles.map((prof) => { + if (prof.did !== requester) return prof + return ctx.services + .local(ctx.db) + .updateProfileDetailed(prof, localProf.record) + }) + return { + ...original, + profiles, + } +} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index 016d5f55487..7c798b7d215 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -4,7 +4,10 @@ import { FeedKeyset } from '../util/feed' import { paginate } from '../../../../../db/pagination' import AppContext from '../../../../../context' import { FeedRow } from '../../../../services/feed' +import { OutputSchema } from '../../../../../lexicon/types/app/bsky/feed/getAuthorFeed' +import { handleReadAfterWrite } from '../util/read-after-write' import { authPassthru } from '../../../../../api/com/atproto/admin/util' +import { LocalRecords } from '../../../../../services/local' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getAuthorFeed({ @@ -19,6 +22,9 @@ export default function (server: Server, ctx: AppContext) { ? await ctx.serviceAuthHeaders(requester) : authPassthru(req), ) + if (requester) { + return await handleReadAfterWrite(ctx, requester, res, getAuthorMunge) + } return { encoding: 'application/json', body: res.data, @@ -128,3 +134,38 @@ async function assertNoBlocks( ) } } + +const getAuthorMunge = async ( + ctx: AppContext, + original: OutputSchema, + local: LocalRecords, + requester: string, +): Promise => { + const localSrvc = ctx.services.local(ctx.db) + const localProf = local.profile + let feed = original.feed + // first update any out of date profile pictures in feed + if (localProf) { + feed = feed.map((item) => { + if (item.post.author.did === requester) { + return { + ...item, + post: { + ...item.post, + author: localSrvc.updateProfileViewBasic( + item.post.author, + localProf.record, + ), + }, + } + } else { + return item + } + }) + } + feed = await localSrvc.formatAndInsertPostsInFeed(feed, local.posts) + return { + ...original, + feed, + } +} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts index 7d0478e1432..bd72c0458c6 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts @@ -1,3 +1,6 @@ +import { AtUri } from '@atproto/uri' +import { AppBskyFeedGetPostThread } from '@atproto/api' +import { Headers } from '@atproto/xrpc' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../../lexicon' import AppContext from '../../../../../context' @@ -15,7 +18,23 @@ import { NotFoundPost, ThreadViewPost, isNotFoundPost, + isThreadViewPost, } from '../../../../../lexicon/types/app/bsky/feed/defs' +import { Record as PostRecord } from '../../../../../lexicon/types/app/bsky/feed/post' +import { + OutputSchema, + QueryParams, +} from '../../../../../lexicon/types/app/bsky/feed/getPostThread' +import { + LocalRecords, + LocalService, + RecordDescript, +} from '../../../../../services/local' +import { + getLocalLag, + getRepoRev, + handleReadAfterWrite, +} from '../util/read-after-write' export type PostThread = { post: FeedRow @@ -29,13 +48,41 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, params, auth }) => { const requester = auth.credentials.did if (ctx.canProxyRead(req)) { - const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( - params, - await ctx.serviceAuthHeaders(requester), - ) - return { - encoding: 'application/json', - body: res.data, + try { + const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( + params, + await ctx.serviceAuthHeaders(requester), + ) + return await handleReadAfterWrite( + ctx, + requester, + res, + getPostThreadMunge, + ) + } catch (err) { + if (err instanceof AppBskyFeedGetPostThread.NotFoundError) { + const local = await readAfterWriteNotFound( + ctx, + params, + requester, + err.headers, + ) + if (local === null) { + throw err + } else { + return { + encoding: 'application/json', + body: local.data, + headers: local.lag + ? { + 'Atproto-Upstream-Lag': local.lag.toString(10), + } + : undefined, + } + } + } else { + throw err + } } } @@ -265,3 +312,145 @@ class ParentNotFoundError extends Error { super(`Parent not found: ${uri}`) } } + +// READ AFTER WRITE +// ---------------- + +const getPostThreadMunge = async ( + ctx: AppContext, + original: OutputSchema, + local: LocalRecords, +): Promise => { + // @TODO if is NotFoundPost, handle similarly to error + // @NOTE not necessary right now as we never return those for the requested uri + if (!isThreadViewPost(original.thread)) { + return original + } + const thread = await addPostsToThread( + ctx.services.local(ctx.db), + original.thread, + local.posts, + ) + return { + ...original, + thread, + } +} + +const addPostsToThread = async ( + localSrvc: LocalService, + original: ThreadViewPost, + posts: RecordDescript[], +) => { + const inThread = findPostsInThread(original, posts) + if (inThread.length === 0) return original + let thread: ThreadViewPost = original + for (const record of inThread) { + thread = await insertIntoThreadReplies(localSrvc, thread, record) + } + return thread +} + +const findPostsInThread = ( + thread: ThreadViewPost, + posts: RecordDescript[], +): RecordDescript[] => { + return posts.filter((post) => { + const rootUri = post.record.reply?.root.uri + if (!rootUri) return false + if (rootUri === thread.post.uri) return true + return (thread.post.record as PostRecord).reply?.root.uri === rootUri + }) +} + +const insertIntoThreadReplies = async ( + localSrvc: LocalService, + view: ThreadViewPost, + descript: RecordDescript, +): Promise => { + if (descript.record.reply?.parent.uri === view.post.uri) { + const postView = await threadPostView(localSrvc, descript) + if (!postView) return view + const replies = [postView, ...(view.replies ?? [])] + return { + ...view, + replies, + } + } + if (!view.replies) return view + const replies = await Promise.all( + view.replies.map(async (reply) => + isThreadViewPost(reply) + ? await insertIntoThreadReplies(localSrvc, reply, descript) + : reply, + ), + ) + return { + ...view, + replies, + } +} + +const threadPostView = async ( + localSrvc: LocalService, + descript: RecordDescript, +): Promise => { + const postView = await localSrvc.getPost(descript) + if (!postView) return null + return { + $type: 'app.bsky.feed.defs#threadViewPost', + post: postView, + } +} + +// Read after write on error +// --------------------- + +const readAfterWriteNotFound = async ( + ctx: AppContext, + params: QueryParams, + requester: string, + headers?: Headers, +): Promise<{ data: OutputSchema; lag?: number } | null> => { + if (!headers) return null + const rev = getRepoRev(headers) + if (!rev) return null + const uri = new AtUri(params.uri) + if (uri.hostname !== requester) { + return null + } + const localSrvc = ctx.services.local(ctx.db) + const local = await localSrvc.getRecordsSinceRev(requester, rev) + const found = local.posts.find((p) => p.uri.toString() === uri.toString()) + if (!found) return null + let thread = await threadPostView(localSrvc, found) + if (!thread) return null + const rest = local.posts.filter((p) => p.uri.toString() !== uri.toString()) + thread = await addPostsToThread(localSrvc, thread, rest) + const highestParent = getHighestParent(thread) + if (highestParent) { + try { + const parentsRes = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( + { uri: highestParent, parentHeight: params.parentHeight, depth: 0 }, + await ctx.serviceAuthHeaders(requester), + ) + thread.parent = parentsRes.data.thread + } catch (err) { + // do nothing + } + } + return { + data: { + thread, + }, + lag: getLocalLag(local), + } +} + +const getHighestParent = (thread: ThreadViewPost): string | undefined => { + if (isThreadViewPost(thread.parent)) { + return getHighestParent(thread.parent) + } else { + return (thread.post.record as PostRecord).reply?.parent.uri + } +} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts b/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts index dba1b58ece7..350165c1387 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts @@ -5,6 +5,9 @@ import { paginate } from '../../../../../db/pagination' import AppContext from '../../../../../context' import { FeedRow } from '../../../../services/feed' import { filterMutesAndBlocks } from './getFeed' +import { OutputSchema } from '../../../../../lexicon/types/app/bsky/feed/getTimeline' +import { handleReadAfterWrite } from '../util/read-after-write' +import { LocalRecords } from '../../../../../services/local' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getTimeline({ @@ -21,10 +24,7 @@ export default function (server: Server, ctx: AppContext) { params, await ctx.serviceAuthHeaders(requester), ) - return { - encoding: 'application/json', - body: res.data, - } + return await handleReadAfterWrite(ctx, requester, res, getTimelineMunge) } if (ctx.cfg.bskyAppViewEndpoint) { @@ -111,3 +111,17 @@ export default function (server: Server, ctx: AppContext) { }, }) } + +const getTimelineMunge = async ( + ctx: AppContext, + original: OutputSchema, + local: LocalRecords, +): Promise => { + const feed = await ctx.services + .local(ctx.db) + .formatAndInsertPostsInFeed([...original.feed], local.posts) + return { + ...original, + feed, + } +} diff --git a/packages/pds/src/app-view/api/app/bsky/util/read-after-write.ts b/packages/pds/src/app-view/api/app/bsky/util/read-after-write.ts new file mode 100644 index 00000000000..3ce7074ce6f --- /dev/null +++ b/packages/pds/src/app-view/api/app/bsky/util/read-after-write.ts @@ -0,0 +1,80 @@ +import { Headers } from '@atproto/xrpc' +import { LocalRecords } from '../../../../../services/local' +import AppContext from '../../../../../context' + +export type ApiRes = { + headers: Headers + data: T +} + +export type MungeFn = ( + ctx: AppContext, + original: T, + local: LocalRecords, + requester: string, +) => Promise + +export type HandlerResponse = { + encoding: 'application/json' + body: T + headers?: Record +} + +export const getRepoRev = (headers: Headers): string | undefined => { + return headers['atproto-repo-rev'] +} + +export const getLocalLag = (local: LocalRecords): number | undefined => { + let oldest: string | undefined = local.profile?.indexedAt + for (const post of local.posts) { + if (!oldest || post.indexedAt < oldest) { + oldest = post.indexedAt + } + } + if (!oldest) return undefined + return Date.now() - new Date(oldest).getTime() +} + +export const handleReadAfterWrite = async ( + ctx: AppContext, + requester: string, + res: ApiRes, + munge: MungeFn, +): Promise> => { + let body: T + let lag: number | undefined = undefined + try { + const withLocal = await readAfterWriteInternal(ctx, requester, res, munge) + body = withLocal.data + lag = withLocal.lag + } catch (err) { + body = res.data + } + return { + encoding: 'application/json', + body, + headers: + lag !== undefined + ? { + 'Atproto-Upstream-Lag': lag.toString(10), + } + : undefined, + } +} + +export const readAfterWriteInternal = async ( + ctx: AppContext, + requester: string, + res: ApiRes, + munge: MungeFn, +): Promise<{ data: T; lag?: number }> => { + const rev = getRepoRev(res.headers) + if (!rev) return { data: res.data } + const localSrvc = ctx.services.local(ctx.db) + const local = await localSrvc.getRecordsSinceRev(requester, rev) + const data = await munge(ctx, res.data, local, requester) + return { + data, + lag: getLocalLag(local), + } +} diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index 0d023008b0d..2b8e870a1cd 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -69,6 +69,7 @@ export interface ServerConfigValues { bskyAppViewModeration?: boolean bskyAppViewDid?: string bskyAppViewProxy: boolean + bskyAppViewCdnUrlPattern?: string crawlersToNotify?: string[] } @@ -218,6 +219,10 @@ export class ServerConfig { const bskyAppViewProxy = process.env.BSKY_APP_VIEW_PROXY === 'true' ? true : false + const bskyAppViewCdnUrlPattern = nonemptyString( + process.env.BSKY_APP_VIEW_CDN_URL_PATTERN, + ) + const crawlersEnv = process.env.CRAWLERS_TO_NOTIFY const crawlersToNotify = crawlersEnv && crawlersEnv.length > 0 ? crawlersEnv.split(',') : [] @@ -274,6 +279,7 @@ export class ServerConfig { bskyAppViewModeration, bskyAppViewDid, bskyAppViewProxy, + bskyAppViewCdnUrlPattern, crawlersToNotify, ...overrides, }) @@ -513,6 +519,10 @@ export class ServerConfig { return this.cfg.bskyAppViewProxy } + get bskyAppViewCdnUrlPattern() { + return this.cfg.bskyAppViewCdnUrlPattern + } + get crawlersToNotify() { return this.cfg.crawlersToNotify } diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index fb966b82e82..74161be1711 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -23,8 +23,6 @@ import { LabelCache } from './label-cache' import { ContentReporter } from './content-reporter' export class AppContext { - private _appviewAgent: AtpAgent | null - constructor( private opts: { db: Database @@ -46,16 +44,11 @@ export class AppContext { labelCache: LabelCache contentReporter?: ContentReporter backgroundQueue: BackgroundQueue + appviewAgent?: AtpAgent crawlers: Crawlers algos: MountedAlgos }, - ) { - this._appviewAgent = opts.cfg.bskyAppViewEndpoint - ? new AtpAgent({ - service: opts.cfg.bskyAppViewEndpoint, - }) - : null - } + ) {} get db(): Database { return this.opts.db @@ -186,10 +179,10 @@ export class AppContext { } get appviewAgent(): AtpAgent { - if (!this._appviewAgent) { + if (!this.opts.appviewAgent) { throw new Error('Could not find bsky appview endpoint') } - return this._appviewAgent + return this.opts.appviewAgent } canProxyRead(req: express.Request): boolean { diff --git a/packages/pds/src/db/migrations/20230808T172813122Z-repo-rev.ts b/packages/pds/src/db/migrations/20230808T172813122Z-repo-rev.ts new file mode 100644 index 00000000000..e4c17d73291 --- /dev/null +++ b/packages/pds/src/db/migrations/20230808T172813122Z-repo-rev.ts @@ -0,0 +1,15 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema.alterTable('record').addColumn('repoRev', 'varchar').execute() + await db.schema + .createIndex('record_repo_rev_idx') + .on('record') + .columns(['did', 'repoRev']) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropIndex('record_repo_rev_idx').execute() + await db.schema.alterTable('record').dropColumn('repoRev').execute() +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index 32a5bfa0f11..bab1041e5cf 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -59,3 +59,4 @@ export * as _20230727T172043676Z from './20230727T172043676Z-user-account-cursor export * as _20230801T141349990Z from './20230801T141349990Z-invite-note' export * as _20230801T195109532Z from './20230801T195109532Z-remove-moderation-fkeys' export * as _20230807T035309811Z from './20230807T035309811Z-feed-item-delete-invite-for-user-idx' +export * as _20230808T172813122Z from './20230808T172813122Z-repo-rev' diff --git a/packages/pds/src/db/tables/record.ts b/packages/pds/src/db/tables/record.ts index 105932276a0..03f1008ef0f 100644 --- a/packages/pds/src/db/tables/record.ts +++ b/packages/pds/src/db/tables/record.ts @@ -5,6 +5,7 @@ export interface Record { did: string collection: string rkey: string + repoRev: string | null indexedAt: string takedownId: number | null } diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index f1e0770e5b4..011949b67c5 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -9,6 +9,7 @@ import cors from 'cors' import http from 'http' import events from 'events' import { createTransport } from 'nodemailer' +import { AtpAgent } from '@atproto/api' import * as crypto from '@atproto/crypto' import { BlobStore } from '@atproto/repo' import { IdResolver } from '@atproto/identity' @@ -205,6 +206,10 @@ export class PDS { }) } + const appviewAgent = config.bskyAppViewEndpoint + ? new AtpAgent({ service: config.bskyAppViewEndpoint }) + : undefined + const services = createServices({ repoSigningKey, messageDispatcher, @@ -214,6 +219,9 @@ export class PDS { labeler, labelCache, contentReporter, + appviewAgent, + appviewDid: config.bskyAppViewDid, + appviewCdnUrlPattern: config.bskyAppViewCdnUrlPattern, backgroundQueue, crawlers, }) @@ -238,6 +246,7 @@ export class PDS { moderationMailer, imgUriBuilder, backgroundQueue, + appviewAgent, crawlers, algos, }) diff --git a/packages/pds/src/services/index.ts b/packages/pds/src/services/index.ts index 528c6de5165..6767b1c535e 100644 --- a/packages/pds/src/services/index.ts +++ b/packages/pds/src/services/index.ts @@ -1,3 +1,4 @@ +import { AtpAgent } from '@atproto/api' import * as crypto from '@atproto/crypto' import { BlobStore } from '@atproto/repo' import Database from '../db' @@ -19,6 +20,7 @@ import { BackgroundQueue } from '../event-stream/background-queue' import { Crawlers } from '../crawlers' import { LabelCache } from '../label-cache' import { ContentReporter } from '../content-reporter' +import { LocalService } from './local' export function createServices(resources: { repoSigningKey: crypto.Keypair @@ -29,6 +31,9 @@ export function createServices(resources: { labeler: Labeler labelCache: LabelCache contentReporter?: ContentReporter + appviewAgent?: AtpAgent + appviewDid?: string + appviewCdnUrlPattern?: string backgroundQueue: BackgroundQueue crawlers: Crawlers }): Services { @@ -41,6 +46,9 @@ export function createServices(resources: { labeler, labelCache, contentReporter, + appviewAgent, + appviewDid, + appviewCdnUrlPattern, backgroundQueue, crawlers, } = resources @@ -57,6 +65,12 @@ export function createServices(resources: { labeler, contentReporter, ), + local: LocalService.creator( + repoSigningKey, + appviewAgent, + appviewDid, + appviewCdnUrlPattern, + ), moderation: ModerationService.creator( messageDispatcher, blobstore, @@ -78,6 +92,7 @@ export type Services = { auth: FromDb record: FromDb repo: FromDb + local: FromDb moderation: FromDb appView: { feed: FromDb diff --git a/packages/pds/src/services/local/index.ts b/packages/pds/src/services/local/index.ts new file mode 100644 index 00000000000..c697d55d010 --- /dev/null +++ b/packages/pds/src/services/local/index.ts @@ -0,0 +1,363 @@ +import util from 'util' +import { CID } from 'multiformats/cid' +import { AtUri } from '@atproto/uri' +import { cborToLexRecord } from '@atproto/repo' +import Database from '../../db' +import { Record as PostRecord } from '../../lexicon/types/app/bsky/feed/post' +import { Record as ProfileRecord } from '../../lexicon/types/app/bsky/actor/profile' +import { ids } from '../../lexicon/lexicons' +import { + ProfileViewBasic, + ProfileView, + ProfileViewDetailed, +} from '../../lexicon/types/app/bsky/actor/defs' +import { FeedViewPost, PostView } from '../../lexicon/types/app/bsky/feed/defs' +import { + Main as EmbedImages, + isMain as isEmbedImages, +} from '../../lexicon/types/app/bsky/embed/images' +import { + Main as EmbedExternal, + isMain as isEmbedExternal, +} from '../../lexicon/types/app/bsky/embed/external' +import { + Main as EmbedRecord, + isMain as isEmbedRecord, + View as EmbedRecordView, +} from '../../lexicon/types/app/bsky/embed/record' +import { + Main as EmbedRecordWithMedia, + isMain as isEmbedRecordWithMedia, +} from '../../lexicon/types/app/bsky/embed/recordWithMedia' +import { AtpAgent } from '@atproto/api' +import { Keypair } from '@atproto/crypto' +import { createServiceAuthHeaders } from '@atproto/xrpc-server' + +type CommonSignedUris = 'avatar' | 'banner' | 'feed_thumbnail' | 'feed_fullsize' + +export class LocalService { + constructor( + public db: Database, + public signingKey: Keypair, + public appviewAgent?: AtpAgent, + public appviewDid?: string, + public appviewCdnUrlPattern?: string, + ) {} + + static creator( + signingKey: Keypair, + appviewAgent?: AtpAgent, + appviewDid?: string, + appviewCdnUrlPattern?: string, + ) { + return (db: Database) => + new LocalService( + db, + signingKey, + appviewAgent, + appviewDid, + appviewCdnUrlPattern, + ) + } + + getImageUrl(pattern: CommonSignedUris, did: string, cid: string) { + if (!this.appviewCdnUrlPattern) { + return '' + } + return util.format(this.appviewCdnUrlPattern, pattern, did, cid) + } + + async serviceAuthHeaders(did: string) { + if (!this.appviewDid) { + throw new Error('Could not find bsky appview did') + } + return createServiceAuthHeaders({ + iss: did, + aud: this.appviewDid, + keypair: this.signingKey, + }) + } + + async getRecordsSinceRev(did: string, rev: string): Promise { + const res = await this.db.db + .selectFrom('record') + .innerJoin('ipld_block', (join) => + join + .onRef('record.did', '=', 'ipld_block.creator') + .onRef('record.cid', '=', 'ipld_block.cid'), + ) + .select([ + 'ipld_block.content', + 'uri', + 'ipld_block.cid', + 'record.indexedAt', + ]) + .where('did', '=', did) + .where('repoRev', '>', rev) + .orderBy('repoRev', 'asc') + .execute() + return res.reduce( + (acc, cur) => { + const descript = { + uri: new AtUri(cur.uri), + cid: CID.parse(cur.cid), + indexedAt: cur.indexedAt, + record: cborToLexRecord(cur.content), + } + if ( + descript.uri.collection === ids.AppBskyActorProfile && + descript.uri.rkey === 'self' + ) { + acc.profile = descript as RecordDescript + } else if (descript.uri.collection === ids.AppBskyFeedPost) { + acc.posts.push(descript as RecordDescript) + } + return acc + }, + { profile: null, posts: [] } as LocalRecords, + ) + } + + async getProfileBasic(did: string): Promise { + const res = await this.db.db + .selectFrom('did_handle') + .leftJoin('record', 'record.did', 'did_handle.did') + .leftJoin('ipld_block', (join) => + join + .onRef('record.did', '=', 'ipld_block.creator') + .onRef('record.cid', '=', 'ipld_block.cid'), + ) + .where('did_handle.did', '=', did) + .where('record.collection', '=', ids.AppBskyActorProfile) + .where('record.rkey', '=', 'self') + .selectAll() + .executeTakeFirst() + if (!res) return null + const record = res.content + ? (cborToLexRecord(res.content) as ProfileRecord) + : null + return { + did, + handle: res.handle, + displayName: record?.displayName, + avatar: record?.avatar + ? this.getImageUrl('avatar', did, record.avatar.ref.toString()) + : undefined, + } + } + + async formatAndInsertPostsInFeed( + feed: FeedViewPost[], + posts: RecordDescript[], + ): Promise { + if (posts.length === 0) { + return feed + } + const lastTime = feed.at(-1)?.post.indexedAt ?? new Date(0).toISOString() + const inFeed = posts.filter((p) => p.indexedAt > lastTime) + const newestToOldest = inFeed.reverse() + const maybeFormatted = await Promise.all( + newestToOldest.map((p) => this.getPost(p)), + ) + const formatted = maybeFormatted.filter((p) => p !== null) as PostView[] + for (const post of formatted) { + const idx = feed.findIndex((fi) => fi.post.indexedAt < post.indexedAt) + if (idx >= 0) { + feed.splice(idx, 0, { post }) + } else { + feed.push({ post }) + } + } + return feed + } + + async getPost( + descript: RecordDescript, + ): Promise { + const { uri, cid, indexedAt, record } = descript + const author = await this.getProfileBasic(uri.hostname) + if (!author) return null + const embed = record.embed + ? await this.formatPostEmbed(author.did, record) + : undefined + return { + uri: uri.toString(), + cid: cid.toString(), + author, + record, + embed: embed ?? undefined, + indexedAt, + } + } + + async formatPostEmbed(did: string, post: PostRecord) { + const embed = post.embed + if (!embed) return null + if (isEmbedImages(embed) || isEmbedExternal(embed)) { + return this.formatSimpleEmbed(did, embed) + } else if (isEmbedRecord(embed)) { + return this.formatRecordEmbed(did, embed) + } else if (isEmbedRecordWithMedia(embed)) { + return this.formatRecordWithMediaEmbed(did, embed) + } else { + return null + } + } + + async formatSimpleEmbed(did: string, embed: EmbedImages | EmbedExternal) { + if (isEmbedImages(embed)) { + const images = embed.images.map((img) => ({ + thumb: this.getImageUrl( + 'feed_thumbnail', + did, + img.image.ref.toString(), + ), + fullsize: this.getImageUrl( + 'feed_fullsize', + did, + img.image.ref.toString(), + ), + alt: img.alt, + })) + return { + $type: 'app.bsky.embed.images#view', + images, + } + } else { + const { uri, title, description, thumb } = embed.external + return { + $type: 'app.bsky.embed.external#view', + uri, + title, + description, + thumb: thumb + ? this.getImageUrl('feed_thumbnail', did, thumb.ref.toString()) + : undefined, + } + } + } + + async formatRecordEmbed( + did: string, + embed: EmbedRecord, + ): Promise { + const view = await this.formatRecordEmbedInternal(did, embed) + return { + $type: 'app.bsky.embed.record#view', + record: + view === null + ? { + $type: 'app.bsky.embed.record#viewNotFound', + uri: embed.record.uri, + } + : view, + } + } + + async formatRecordEmbedInternal(did: string, embed: EmbedRecord) { + if (!this.appviewAgent || !this.appviewDid) { + return null + } + const collection = new AtUri(embed.record.uri).collection + if (collection === ids.AppBskyFeedPost) { + const res = await this.appviewAgent.api.app.bsky.feed.getPosts( + { + uris: [embed.record.uri], + }, + await this.serviceAuthHeaders(did), + ) + const post = res.data.posts[0] + if (!post) return null + return { + $type: 'app.bsky.embed.record#viewRecord', + uri: post.uri, + cid: post.cid, + author: post.author, + value: post.record, + labels: post.labels, + embeds: post.embed ? [post.embed] : undefined, + indexedAt: post.indexedAt, + } + } else if (collection === ids.AppBskyFeedGenerator) { + const res = await this.appviewAgent.api.app.bsky.feed.getFeedGenerator( + { + feed: embed.record.uri, + }, + await this.serviceAuthHeaders(did), + ) + return { + $type: 'app.bsaky.feed.defs#generatorView', + ...res.data.view, + } + } else if (collection === ids.AppBskyGraphList) { + const res = await this.appviewAgent.api.app.bsky.graph.getList( + { + list: embed.record.uri, + }, + await this.serviceAuthHeaders(did), + ) + return { + $type: 'app.bsaky.graph.defs#listView', + ...res.data.list, + } + } + return null + } + + async formatRecordWithMediaEmbed(did: string, embed: EmbedRecordWithMedia) { + if (!isEmbedImages(embed.media) && !isEmbedExternal(embed.media)) { + return null + } + const media = this.formatSimpleEmbed(did, embed.media) + const record = await this.formatRecordEmbed(did, embed.record) + return { + $type: 'app.bsky.embed.recordWithMedia#view', + record, + media, + } + } + + updateProfileViewBasic( + view: ProfileViewBasic, + record: ProfileRecord, + ): ProfileViewBasic { + return { + ...view, + displayName: record.displayName, + avatar: record.avatar + ? this.getImageUrl('avatar', view.did, record.avatar.ref.toString()) + : undefined, + } + } + + updateProfileView(view: ProfileView, record: ProfileRecord): ProfileView { + return { + ...this.updateProfileViewBasic(view, record), + description: record.description, + } + } + + updateProfileDetailed( + view: ProfileViewDetailed, + record: ProfileRecord, + ): ProfileViewDetailed { + return { + ...this.updateProfileView(view, record), + banner: record.banner + ? this.getImageUrl('banner', view.did, record.banner.ref.toString()) + : undefined, + } + } +} + +export type LocalRecords = { + profile: RecordDescript | null + posts: RecordDescript[] +} + +export type RecordDescript = { + uri: AtUri + cid: CID + indexedAt: string + record: T +} diff --git a/packages/pds/src/services/record/index.ts b/packages/pds/src/services/record/index.ts index 5dd94914d43..604a6e7b71c 100644 --- a/packages/pds/src/services/record/index.ts +++ b/packages/pds/src/services/record/index.ts @@ -26,6 +26,7 @@ export class RecordService { cid: CID, obj: unknown, action: WriteOpAction.Create | WriteOpAction.Update = WriteOpAction.Create, + repoRev?: string, timestamp?: string, ) { this.db.assertTransaction() @@ -36,6 +37,7 @@ export class RecordService { did: uri.host, collection: uri.collection, rkey: uri.rkey, + repoRev: repoRev ?? null, indexedAt: timestamp || new Date().toISOString(), } if (!record.did.startsWith('did:')) { @@ -51,9 +53,11 @@ export class RecordService { .insertInto('record') .values(record) .onConflict((oc) => - oc - .column('uri') - .doUpdateSet({ cid: record.cid, indexedAt: record.indexedAt }), + oc.column('uri').doUpdateSet({ + cid: record.cid, + repoRev: repoRev ?? null, + indexedAt: record.indexedAt, + }), ) .execute() diff --git a/packages/pds/src/services/repo/index.ts b/packages/pds/src/services/repo/index.ts index 5e9a3116d3b..50d60872a8a 100644 --- a/packages/pds/src/services/repo/index.ts +++ b/packages/pds/src/services/repo/index.ts @@ -125,7 +125,7 @@ export class RepoService { // persist the commit to repo storage storage.applyCommit(commitData), // & send to indexing - this.indexWrites(writes, now), + this.indexWrites(writes, now, commitData.rev), // process blobs this.blobs.processWriteBlobs(did, commitData.commit, writes), // do any other processing needed after write @@ -204,7 +204,7 @@ export class RepoService { return repo.formatCommit(writeOps, this.repoSigningKey) } - async indexWrites(writes: PreparedWrite[], now: string) { + async indexWrites(writes: PreparedWrite[], now: string, rev?: string) { this.db.assertTransaction() const recordTxn = this.services.record(this.db) await Promise.all( @@ -218,6 +218,7 @@ export class RepoService { write.cid, write.record, write.action, + rev, now, ) } else if (write.action === WriteOpAction.Delete) { diff --git a/packages/pds/tests/proxied/read-after-write.test.ts b/packages/pds/tests/proxied/read-after-write.test.ts new file mode 100644 index 00000000000..bc0a7d49681 --- /dev/null +++ b/packages/pds/tests/proxied/read-after-write.test.ts @@ -0,0 +1,178 @@ +import util from 'util' +import AtpAgent from '@atproto/api' +import { TestNetwork } from '@atproto/dev-env' +import { RecordRef, SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' +import { ThreadViewPost } from '../../src/lexicon/types/app/bsky/feed/defs' +import { View as RecordEmbedView } from '../../src/lexicon/types/app/bsky/embed/record' + +describe('proxy read after write', () => { + let network: TestNetwork + let agent: AtpAgent + let sc: SeedClient + + let alice: string + let carol: string + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'proxy_read_after_write', + }) + agent = network.pds.getClient() + sc = new SeedClient(agent) + await basicSeed(sc) + await network.processAll() + alice = sc.dids.alice + carol = sc.dids.carol + await network.bsky.sub.destroy() + }) + + afterAll(async () => { + await network.close() + }) + + it('handles read after write on profiles', async () => { + await sc.updateProfile(alice, { displayName: 'blah' }) + const res = await agent.api.app.bsky.actor.getProfile( + { actor: alice }, + { headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' } }, + ) + expect(res.data.displayName).toEqual('blah') + expect(res.data.description).toBeUndefined() + }) + + it('handles image formatting', async () => { + const blob = await sc.uploadFile( + alice, + 'tests/image/fixtures/key-landscape-small.jpg', + 'image/jpeg', + ) + await sc.updateProfile(alice, { displayName: 'blah', avatar: blob.image }) + + const res = await agent.api.app.bsky.actor.getProfile( + { actor: alice }, + { headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' } }, + ) + expect(res.data.avatar).toEqual( + util.format( + network.pds.ctx.cfg.bskyAppViewCdnUrlPattern, + 'avatar', + alice, + blob.image.ref.toString(), + ), + ) + }) + + it('handles read after write on getAuthorFeed', async () => { + const res = await agent.api.app.bsky.feed.getAuthorFeed( + { actor: alice }, + { headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' } }, + ) + for (const item of res.data.feed) { + if (item.post.author.did === alice) { + expect(item.post.author.displayName).toEqual('blah') + } + } + }) + + let replyRef1: RecordRef + let replyRef2: RecordRef + + it('handles read after write on threads', async () => { + const reply1 = await sc.reply( + alice, + sc.posts[alice][0].ref, + sc.posts[alice][0].ref, + 'another reply', + ) + const reply2 = await sc.reply( + alice, + sc.posts[alice][0].ref, + reply1.ref, + 'another another reply', + ) + replyRef1 = reply1.ref + replyRef2 = reply2.ref + const res = await agent.api.app.bsky.feed.getPostThread( + { uri: sc.posts[alice][0].ref.uriStr }, + { headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' } }, + ) + const layerOne = res.data.thread.replies as ThreadViewPost[] + expect(layerOne.length).toBe(1) + expect(layerOne[0].post.uri).toEqual(reply1.ref.uriStr) + const layerTwo = layerOne[0].replies as ThreadViewPost[] + expect(layerTwo.length).toBe(1) + expect(layerTwo[0].post.uri).toEqual(reply2.ref.uriStr) + }) + + it('handles read after write on a thread that is not found on appview', async () => { + const res = await agent.api.app.bsky.feed.getPostThread( + { uri: replyRef1.uriStr }, + { headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' } }, + ) + const thread = res.data.thread as ThreadViewPost + expect(thread.post.uri).toEqual(replyRef1.uriStr) + expect((thread.parent as ThreadViewPost).post.uri).toEqual( + sc.posts[alice][0].ref.uriStr, + ) + expect(thread.replies?.length).toEqual(1) + expect((thread.replies?.at(0) as ThreadViewPost).post.uri).toEqual( + replyRef2.uriStr, + ) + }) + + it('handles read after write on threads with record embeds', async () => { + const replyRes = await agent.api.app.bsky.feed.post.create( + { repo: alice }, + { + text: 'blah', + reply: { + root: sc.posts[carol][0].ref.raw, + parent: sc.posts[carol][0].ref.raw, + }, + embed: { + $type: 'app.bsky.embed.record', + record: sc.posts[alice][0].ref.raw, + }, + createdAt: new Date().toISOString(), + }, + sc.getHeaders(alice), + ) + const res = await agent.api.app.bsky.feed.getPostThread( + { uri: sc.posts[carol][0].ref.uriStr }, + { headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' } }, + ) + const replies = res.data.thread.replies as ThreadViewPost[] + expect(replies.length).toBe(1) + expect(replies[0].post.uri).toEqual(replyRes.uri) + const embed = replies[0].post.embed as RecordEmbedView + expect(embed.record.uri).toEqual(sc.posts[alice][0].ref.uriStr) + }) + + it('handles read after write on getTimeline', async () => { + const postRes = await agent.api.app.bsky.feed.post.create( + { repo: alice }, + { + text: 'poast', + createdAt: new Date().toISOString(), + }, + sc.getHeaders(alice), + ) + const res = await agent.api.app.bsky.feed.getTimeline( + {}, + { headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' } }, + ) + expect(res.data.feed[0].post.uri).toEqual(postRes.uri) + }) + + it('returns lag headers', async () => { + const res = await agent.api.app.bsky.feed.getTimeline( + {}, + { headers: { ...sc.getHeaders(alice), 'x-appview-proxy': 'true' } }, + ) + const lag = res.headers['atproto-upstream-lag'] + expect(lag).toBeDefined() + const parsed = parseInt(lag) + expect(parsed > 0).toBe(true) + }) +}) diff --git a/packages/pds/tests/seeds/client.ts b/packages/pds/tests/seeds/client.ts index 82cb02571eb..59735d865f5 100644 --- a/packages/pds/tests/seeds/client.ts +++ b/packages/pds/tests/seeds/client.ts @@ -10,6 +10,7 @@ import { Record as FollowRecord } from '@atproto/api/src/client/types/app/bsky/g import { AtUri } from '@atproto/uri' import { BlobRef } from '@atproto/lexicon' import { adminAuth } from '../_util' +import { ids } from '../../src/lexicon/lexicons' // Makes it simple to create data via the XRPC client, // and keeps track of all created data in memory for convenience. @@ -162,6 +163,24 @@ export class SeedClient { return this.profiles[by] } + async updateProfile(by: string, record: Record) { + const res = await this.agent.api.com.atproto.repo.putRecord( + { + repo: by, + collection: ids.AppBskyActorProfile, + rkey: 'self', + record, + }, + { headers: this.getHeaders(by), encoding: 'application/json' }, + ) + this.profiles[by] = { + ...(this.profiles[by] ?? {}), + ...record, + ref: new RecordRef(res.data.uri, res.data.cid), + } + return this.profiles[by] + } + async follow(from: string, to: string, overrides?: Partial) { const res = await this.agent.api.app.bsky.graph.follow.create( { repo: from }, diff --git a/packages/repo/src/repo.ts b/packages/repo/src/repo.ts index 78dbe7409d4..181944ec1d9 100644 --- a/packages/repo/src/repo.ts +++ b/packages/repo/src/repo.ts @@ -1,4 +1,5 @@ import { CID } from 'multiformats/cid' +import { TID } from '@atproto/common' import * as crypto from '@atproto/crypto' import { Commit, @@ -147,12 +148,14 @@ export class Repo extends ReadableRepo { commitBlocks.addMap(fromStorage.blocks) } + const rev = TID.nextStr(this.commit.rev) const commit = await util.signCommit( { did: this.did, version: 2, prev: this.cid, data: unstoredData.root, + rev, }, keypair, ) @@ -161,6 +164,7 @@ export class Repo extends ReadableRepo { return { commit: commitCid, prev: this.cid, + rev, blocks: commitBlocks, } } diff --git a/packages/repo/src/types.ts b/packages/repo/src/types.ts index 24871288295..3cc3e8801ab 100644 --- a/packages/repo/src/types.ts +++ b/packages/repo/src/types.ts @@ -12,6 +12,7 @@ const unsignedCommit = z.object({ version: z.number(), prev: common.cid.nullable(), data: common.cid, + rev: z.string().optional(), }) export type UnsignedCommit = z.infer & { sig?: never } @@ -20,6 +21,7 @@ const commit = z.object({ version: z.number(), prev: common.cid.nullable(), data: common.cid, + rev: z.string().optional(), sig: common.bytes, }) export type Commit = z.infer @@ -98,6 +100,7 @@ export type CommitBlockData = { export type CommitData = CommitBlockData & { prev: CID | null + rev?: string } export type RebaseData = { diff --git a/packages/xrpc/src/client.ts b/packages/xrpc/src/client.ts index c6953d1a415..fbb5d33662c 100644 --- a/packages/xrpc/src/client.ts +++ b/packages/xrpc/src/client.ts @@ -123,7 +123,12 @@ export class ServiceClient { return new XRPCResponse(res.body, res.headers) } else { if (res.body && isErrorResponseBody(res.body)) { - throw new XRPCError(resCode, res.body.error, res.body.message) + throw new XRPCError( + resCode, + res.body.error, + res.body.message, + res.headers, + ) } else { throw new XRPCError(resCode) } diff --git a/packages/xrpc/src/types.ts b/packages/xrpc/src/types.ts index 181aa9b9264..9b79f3cd21c 100644 --- a/packages/xrpc/src/types.ts +++ b/packages/xrpc/src/types.ts @@ -85,16 +85,19 @@ export class XRPCResponse { export class XRPCError extends Error { success = false + headers?: Headers constructor( public status: ResponseType, public error?: string, message?: string, + headers?: Headers, ) { super(message || error || ResponseTypeStrings[status]) if (!this.error) { this.error = ResponseTypeNames[status] } + this.headers = headers } } From ba979b4cdb30de7f2b4c2f32732044601110ef7e Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 10 Aug 2023 14:31:45 -0500 Subject: [PATCH 127/237] Include limited viewer info on blocked post (#1457) * include limited viewer info on block * blockedAuthor schema * make author required * merge & update codegen * patch up tests --- lexicons/app/bsky/feed/defs.json | 13 ++++++-- packages/api/src/client/lexicons.ts | 20 +++++++++++- .../src/client/types/app/bsky/feed/defs.ts | 19 ++++++++++++ .../src/api/app/bsky/feed/getPostThread.ts | 9 ++++++ packages/bsky/src/lexicon/lexicons.ts | 20 +++++++++++- .../src/lexicon/types/app/bsky/feed/defs.ts | 19 ++++++++++++ packages/bsky/src/services/feed/views.ts | 15 +++++++-- .../views/__snapshots__/blocks.test.ts.snap | 29 ++++++++++++----- packages/bsky/tests/views/blocks.test.ts | 15 +++++++++ .../api/app/bsky/feed/getPostThread.ts | 9 ++++++ .../pds/src/app-view/services/feed/views.ts | 15 +++++++-- packages/pds/src/lexicon/lexicons.ts | 20 +++++++++++- .../src/lexicon/types/app/bsky/feed/defs.ts | 19 ++++++++++++ .../views/__snapshots__/blocks.test.ts.snap | 31 +++++++++++++------ packages/pds/tests/views/blocks.test.ts | 15 +++++++++ 15 files changed, 240 insertions(+), 28 deletions(-) diff --git a/lexicons/app/bsky/feed/defs.json b/lexicons/app/bsky/feed/defs.json index a4fe08aec8b..0dd61eadcba 100644 --- a/lexicons/app/bsky/feed/defs.json +++ b/lexicons/app/bsky/feed/defs.json @@ -84,10 +84,19 @@ }, "blockedPost": { "type": "object", - "required": ["uri", "blocked"], + "required": ["uri", "blocked", "author"], "properties": { "uri": {"type": "string", "format": "at-uri"}, - "blocked": {"type": "boolean", "const": true} + "blocked": {"type": "boolean", "const": true}, + "author": {"type": "ref", "ref": "#blockedAuthor"} + } + }, + "blockedAuthor": { + "type": "object", + "required": ["did"], + "properties": { + "did": {"type": "string", "format": "did"}, + "viewer": { "type": "ref", "ref": "app.bsky.actor.defs#viewerState" } } }, "generatorView": { diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 780238d5a42..400bc76a24a 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -4512,7 +4512,7 @@ export const schemaDict = { }, blockedPost: { type: 'object', - required: ['uri', 'blocked'], + required: ['uri', 'blocked', 'author'], properties: { uri: { type: 'string', @@ -4522,6 +4522,24 @@ export const schemaDict = { 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: { diff --git a/packages/api/src/client/types/app/bsky/feed/defs.ts b/packages/api/src/client/types/app/bsky/feed/defs.ts index 5aa51290fed..1270dab250b 100644 --- a/packages/api/src/client/types/app/bsky/feed/defs.ts +++ b/packages/api/src/client/types/app/bsky/feed/defs.ts @@ -171,6 +171,7 @@ export function validateNotFoundPost(v: unknown): ValidationResult { export interface BlockedPost { uri: string blocked: true + author: BlockedAuthor [k: string]: unknown } @@ -186,6 +187,24 @@ 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 diff --git a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts index ca7e0bbbcdc..677200bd387 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts @@ -111,6 +111,15 @@ const composeThread = ( $type: 'app.bsky.feed.defs#blockedPost', uri: threadData.post.postUri, blocked: true, + author: { + did: post.author.did, + viewer: post.author.viewer + ? { + blockedBy: post.author.viewer?.blockedBy, + blocking: post.author.viewer?.blocking, + } + : undefined, + }, } } diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 780238d5a42..400bc76a24a 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -4512,7 +4512,7 @@ export const schemaDict = { }, blockedPost: { type: 'object', - required: ['uri', 'blocked'], + required: ['uri', 'blocked', 'author'], properties: { uri: { type: 'string', @@ -4522,6 +4522,24 @@ export const schemaDict = { 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: { diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/defs.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/defs.ts index 80069e4e412..463445fbd49 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/defs.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/defs.ts @@ -171,6 +171,7 @@ export function validateNotFoundPost(v: unknown): ValidationResult { export interface BlockedPost { uri: string blocked: true + author: BlockedAuthor [k: string]: unknown } @@ -186,6 +187,24 @@ 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 diff --git a/packages/bsky/src/services/feed/views.ts b/packages/bsky/src/services/feed/views.ts index dde89d5762e..6bef3e9182e 100644 --- a/packages/bsky/src/services/feed/views.ts +++ b/packages/bsky/src/services/feed/views.ts @@ -214,7 +214,7 @@ export class FeedViews { blocks[uri]?.reply ) { if (!usePostViewUnion) return - return this.blockedPost(uri) + return this.blockedPost(post) } return { $type: 'app.bsky.feed.defs#postView', @@ -222,11 +222,20 @@ export class FeedViews { } } - blockedPost(uri: string) { + blockedPost(post: PostView) { return { $type: 'app.bsky.feed.defs#blockedPost', - uri: uri, + uri: post.uri, blocked: true as const, + author: { + did: post.author.did, + viewer: post.author.viewer + ? { + blockedBy: post.author.viewer?.blockedBy, + blocking: post.author.viewer?.blocking, + } + : undefined, + }, } } diff --git a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap index 812959c5497..d54a3e7270d 100644 --- a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap @@ -128,6 +128,12 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "parent": Object { "$type": "app.bsky.feed.defs#blockedPost", + "author": Object { + "did": "user(2)", + "viewer": Object { + "blockedBy": true, + }, + }, "blocked": true, "uri": "record(4)", }, @@ -245,6 +251,13 @@ Object { "replies": Array [ Object { "$type": "app.bsky.feed.defs#blockedPost", + "author": Object { + "did": "user(2)", + "viewer": Object { + "blockedBy": false, + "blocking": "record(6)", + }, + }, "blocked": true, "uri": "record(5)", }, @@ -252,14 +265,14 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { "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": "bobby", "handle": "bob.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(7)", + "following": "record(8)", "muted": false, }, }, @@ -269,8 +282,8 @@ Object { "images": Array [ Object { "alt": "tests/image/fixtures/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", + "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", }, ], }, @@ -281,7 +294,7 @@ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "test-label", }, Object { @@ -289,7 +302,7 @@ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "test-label-2", }, ], @@ -327,7 +340,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(6)", + "uri": "record(7)", "viewer": Object {}, }, }, diff --git a/packages/bsky/tests/views/blocks.test.ts b/packages/bsky/tests/views/blocks.test.ts index 691487f9fc7..127aac4fc6b 100644 --- a/packages/bsky/tests/views/blocks.test.ts +++ b/packages/bsky/tests/views/blocks.test.ts @@ -24,6 +24,7 @@ describe('pds views with blocking', () => { let alice: string let carol: string let dan: string + let danBlockUri: string beforeAll(async () => { network = await TestNetwork.create({ @@ -57,6 +58,7 @@ describe('pds views with blocking', () => { { createdAt: new Date().toISOString(), subject: carol }, sc.getHeaders(dan), ) + danBlockUri = danBlockCarol.uri await network.processAll() }) @@ -74,6 +76,13 @@ describe('pds views with blocking', () => { $type: 'app.bsky.feed.defs#blockedPost', uri: sc.posts[carol][0].ref.uriStr, blocked: true, + author: { + did: carol, + viewer: { + blockedBy: false, + blocking: danBlockUri, + }, + }, }, }) const { data: threadCarol } = await agent.api.app.bsky.feed.getPostThread( @@ -85,6 +94,12 @@ describe('pds views with blocking', () => { $type: 'app.bsky.feed.defs#blockedPost', uri: sc.posts[dan][0].ref.uriStr, blocked: true, + author: { + did: dan, + viewer: { + blockedBy: true, + }, + }, }, }) }) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts index bd72c0458c6..009eb4bafed 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts @@ -164,6 +164,15 @@ const composeThread = ( $type: 'app.bsky.feed.defs#blockedPost', uri: threadData.post.postUri, blocked: true, + author: { + did: post.author.did, + viewer: post.author.viewer + ? { + blockedBy: post.author.viewer?.blockedBy, + blocking: post.author.viewer?.blocking, + } + : undefined, + }, } } diff --git a/packages/pds/src/app-view/services/feed/views.ts b/packages/pds/src/app-view/services/feed/views.ts index 846fa928a19..d1859fc7d1b 100644 --- a/packages/pds/src/app-view/services/feed/views.ts +++ b/packages/pds/src/app-view/services/feed/views.ts @@ -214,7 +214,7 @@ export class FeedViews { blocks[uri]?.reply ) { if (!opts?.usePostViewUnion) return - return this.blockedPost(uri) + return this.blockedPost(post) } return { $type: 'app.bsky.feed.defs#postView', @@ -222,11 +222,20 @@ export class FeedViews { } } - blockedPost(uri: string) { + blockedPost(post: PostView) { return { $type: 'app.bsky.feed.defs#blockedPost', - uri: uri, + uri: post.uri, blocked: true as const, + author: { + did: post.author.did, + viewer: post.author.viewer + ? { + blockedBy: post.author.viewer?.blockedBy, + blocking: post.author.viewer?.blocking, + } + : undefined, + }, } } diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 780238d5a42..400bc76a24a 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -4512,7 +4512,7 @@ export const schemaDict = { }, blockedPost: { type: 'object', - required: ['uri', 'blocked'], + required: ['uri', 'blocked', 'author'], properties: { uri: { type: 'string', @@ -4522,6 +4522,24 @@ export const schemaDict = { 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: { diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/defs.ts b/packages/pds/src/lexicon/types/app/bsky/feed/defs.ts index 80069e4e412..463445fbd49 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/defs.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/defs.ts @@ -171,6 +171,7 @@ export function validateNotFoundPost(v: unknown): ValidationResult { export interface BlockedPost { uri: string blocked: true + author: BlockedAuthor [k: string]: unknown } @@ -186,6 +187,24 @@ 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 diff --git a/packages/pds/tests/views/__snapshots__/blocks.test.ts.snap b/packages/pds/tests/views/__snapshots__/blocks.test.ts.snap index d29d658bc5d..adcf8f45b77 100644 --- a/packages/pds/tests/views/__snapshots__/blocks.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/blocks.test.ts.snap @@ -136,6 +136,12 @@ Object { "$type": "app.bsky.feed.defs#threadViewPost", "parent": Object { "$type": "app.bsky.feed.defs#blockedPost", + "author": Object { + "did": "user(1)", + "viewer": Object { + "blockedBy": true, + }, + }, "blocked": true, "uri": "record(4)", }, @@ -253,6 +259,13 @@ Object { "replies": Array [ Object { "$type": "app.bsky.feed.defs#blockedPost", + "author": Object { + "did": "user(1)", + "viewer": Object { + "blockedBy": false, + "blocking": "record(6)", + }, + }, "blocked": true, "uri": "record(5)", }, @@ -261,7 +274,7 @@ Object { "post": Object { "author": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(1)", + "did": "user(2)", "displayName": "bobby", "handle": "bob.test", "labels": Array [ @@ -269,22 +282,22 @@ Object { "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(1)", - "uri": "record(8)", + "src": "user(2)", + "uri": "record(9)", "val": "self-label-a", }, Object { "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(1)", - "uri": "record(8)", + "src": "user(2)", + "uri": "record(9)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "following": "record(7)", + "following": "record(8)", "muted": false, }, }, @@ -306,7 +319,7 @@ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "test-label", }, Object { @@ -314,7 +327,7 @@ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(6)", + "uri": "record(7)", "val": "test-label-2", }, ], @@ -352,7 +365,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(6)", + "uri": "record(7)", "viewer": Object {}, }, }, diff --git a/packages/pds/tests/views/blocks.test.ts b/packages/pds/tests/views/blocks.test.ts index b56f80cd18e..8d8fb3dba76 100644 --- a/packages/pds/tests/views/blocks.test.ts +++ b/packages/pds/tests/views/blocks.test.ts @@ -24,6 +24,7 @@ describe('pds views with blocking', () => { let alice: string let carol: string let dan: string + let danBlockUri: string beforeAll(async () => { server = await runTestServer({ @@ -57,6 +58,7 @@ describe('pds views with blocking', () => { { createdAt: new Date().toISOString(), subject: carol }, sc.getHeaders(dan), ) + danBlockUri = danBlockCarol.uri await server.processAll() }) @@ -74,6 +76,13 @@ describe('pds views with blocking', () => { $type: 'app.bsky.feed.defs#blockedPost', uri: sc.posts[carol][0].ref.uriStr, blocked: true, + author: { + did: carol, + viewer: { + blockedBy: false, + blocking: danBlockUri, + }, + }, }, }) const { data: threadCarol } = await agent.api.app.bsky.feed.getPostThread( @@ -85,6 +94,12 @@ describe('pds views with blocking', () => { $type: 'app.bsky.feed.defs#blockedPost', uri: sc.posts[dan][0].ref.uriStr, blocked: true, + author: { + did: dan, + viewer: { + blockedBy: true, + }, + }, }, }) }) From 29a1ee791ab0d3b151dfbd3734868832d18e3f61 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 10 Aug 2023 12:53:35 -0700 Subject: [PATCH 128/237] @atproto/api@0.6.2 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 9a1a56f4a7c..8b637b2620a 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.0", + "version": "0.6.2", "main": "src/index.ts", "scripts": { "codegen": "yarn docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From 244bf46e741c88b0eca06ad2c937d4a48264a413 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 10 Aug 2023 15:54:24 -0500 Subject: [PATCH 129/237] Include limited info on blocked embeds (#1463) * add block info to embeds * fix codegen * Correctly handle blocked embeds and add block-other cause * update snaps * Correctly identify blocking behavior in embeds --------- Co-authored-by: Paul Frazee --- lexicons/app/bsky/embed/record.json | 11 +++++++---- packages/api/src/client/lexicons.ts | 16 ++++++++++++++-- .../src/client/types/app/bsky/embed/record.ts | 3 +++ packages/api/src/moderation/accumulator.ts | 16 +++++++++++++++- .../api/src/moderation/subjects/quoted-post.ts | 18 ++++++++++++++++++ packages/api/src/moderation/types.ts | 1 + packages/api/src/moderation/util.ts | 16 ++-------------- packages/bsky/src/lexicon/lexicons.ts | 16 ++++++++++++++-- .../src/lexicon/types/app/bsky/embed/record.ts | 3 +++ packages/bsky/src/services/feed/index.ts | 11 +++++++++++ packages/bsky/src/services/feed/views.ts | 11 +++++++++++ .../__snapshots__/feed-generation.test.ts.snap | 1 + .../views/__snapshots__/blocks.test.ts.snap | 8 ++++++++ .../views/__snapshots__/timeline.test.ts.snap | 3 +++ .../pds/src/app-view/services/feed/index.ts | 11 +++++++++++ .../pds/src/app-view/services/feed/views.ts | 11 +++++++++++ packages/pds/src/lexicon/lexicons.ts | 16 ++++++++++++++-- .../src/lexicon/types/app/bsky/embed/record.ts | 3 +++ .../__snapshots__/feed-generation.test.ts.snap | 1 + .../views/__snapshots__/blocks.test.ts.snap | 8 ++++++++ .../views/__snapshots__/timeline.test.ts.snap | 3 +++ 21 files changed, 162 insertions(+), 25 deletions(-) diff --git a/lexicons/app/bsky/embed/record.json b/lexicons/app/bsky/embed/record.json index 0b55be0c9c2..3cd27d49503 100644 --- a/lexicons/app/bsky/embed/record.json +++ b/lexicons/app/bsky/embed/record.json @@ -55,16 +55,19 @@ }, "viewNotFound": { "type": "object", - "required": ["uri"], + "required": ["uri", "notFound"], "properties": { - "uri": {"type": "string", "format": "at-uri"} + "uri": {"type": "string", "format": "at-uri"}, + "notFound": {"type": "boolean", "const": true} } }, "viewBlocked": { "type": "object", - "required": ["uri"], + "required": ["uri", "blocked", "author"], "properties": { - "uri": {"type": "string", "format": "at-uri"} + "uri": {"type": "string", "format": "at-uri"}, + "blocked": {"type": "boolean", "const": true}, + "author": {"type": "ref", "ref": "app.bsky.feed.defs#blockedAuthor"} } } } diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 400bc76a24a..956a481627c 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -4283,22 +4283,34 @@ export const schemaDict = { }, viewNotFound: { type: 'object', - required: ['uri'], + required: ['uri', 'notFound'], properties: { uri: { type: 'string', format: 'at-uri', }, + notFound: { + type: 'boolean', + const: true, + }, }, }, viewBlocked: { type: 'object', - required: ['uri'], + 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', + }, }, }, }, diff --git a/packages/api/src/client/types/app/bsky/embed/record.ts b/packages/api/src/client/types/app/bsky/embed/record.ts index 223669e03b5..caee8f08cdd 100644 --- a/packages/api/src/client/types/app/bsky/embed/record.ts +++ b/packages/api/src/client/types/app/bsky/embed/record.ts @@ -84,6 +84,7 @@ export function validateViewRecord(v: unknown): ValidationResult { export interface ViewNotFound { uri: string + notFound: true [k: string]: unknown } @@ -101,6 +102,8 @@ export function validateViewNotFound(v: unknown): ValidationResult { export interface ViewBlocked { uri: string + blocked: true + author: AppBskyFeedDefs.BlockedAuthor [k: string]: unknown } diff --git a/packages/api/src/moderation/accumulator.ts b/packages/api/src/moderation/accumulator.ts index 22a24b47119..eebdfd59399 100644 --- a/packages/api/src/moderation/accumulator.ts +++ b/packages/api/src/moderation/accumulator.ts @@ -38,6 +38,16 @@ export class ModerationCauseAccumulator { } } + addBlockOther(blockOther: boolean | undefined) { + if (blockOther) { + this.causes.push({ + type: 'block-other', + source: { type: 'user' }, + priority: 4, + }) + } + } + addLabel(label: Label, opts: ModerationOpts) { // look up the label definition const labelDef = LABELS[label.val] @@ -134,7 +144,11 @@ export class ModerationCauseAccumulator { mod.additionalCauses = this.causes.slice(1) // blocked user - if (mod.cause.type === 'blocking' || mod.cause.type === 'blocked-by') { + if ( + mod.cause.type === 'blocking' || + mod.cause.type === 'blocked-by' || + mod.cause.type === 'block-other' + ) { // filter and blur, dont allow override mod.filter = true mod.blur = true diff --git a/packages/api/src/moderation/subjects/quoted-post.ts b/packages/api/src/moderation/subjects/quoted-post.ts index 8aac01626a3..6d0f9eb9d52 100644 --- a/packages/api/src/moderation/subjects/quoted-post.ts +++ b/packages/api/src/moderation/subjects/quoted-post.ts @@ -17,6 +17,15 @@ export function decideQuotedPost( acc.addLabel(label, opts) } } + } else if (AppBskyEmbedRecord.isViewBlocked(subject.record)) { + acc.setDid(subject.record.author.did) + if (subject.record.author.viewer?.blocking) { + acc.addBlocking(subject.record.author.viewer?.blocking) + } else if (subject.record.author.viewer?.blockedBy) { + acc.addBlockedBy(subject.record.author.viewer?.blockedBy) + } else { + acc.addBlockOther(true) + } } return acc.finalizeDecision(opts) @@ -46,6 +55,15 @@ export function decideQuotedPostWithMedia( acc.addLabel(label, opts) } } + } else if (AppBskyEmbedRecord.isViewBlocked(subject.record.record)) { + acc.setDid(subject.record.record.author.did) + if (subject.record.record.author.viewer?.blocking) { + acc.addBlocking(subject.record.record.author.viewer?.blocking) + } else if (subject.record.record.author.viewer?.blockedBy) { + acc.addBlockedBy(subject.record.record.author.viewer?.blockedBy) + } else { + acc.addBlockOther(true) + } } return acc.finalizeDecision(opts) diff --git a/packages/api/src/moderation/types.ts b/packages/api/src/moderation/types.ts index 64f39505df1..e3cb6200a00 100644 --- a/packages/api/src/moderation/types.ts +++ b/packages/api/src/moderation/types.ts @@ -100,6 +100,7 @@ export type ModerationCauseSource = export type ModerationCause = | { type: 'blocking'; source: ModerationCauseSource; priority: 3 } | { type: 'blocked-by'; source: ModerationCauseSource; priority: 4 } + | { type: 'block-other'; source: ModerationCauseSource; priority: 4 } | { type: 'label' source: ModerationCauseSource diff --git a/packages/api/src/moderation/util.ts b/packages/api/src/moderation/util.ts index f1f6b9f07f7..7b42f4dfffe 100644 --- a/packages/api/src/moderation/util.ts +++ b/packages/api/src/moderation/util.ts @@ -70,25 +70,13 @@ export function isModerationDecisionNoop( } export function isQuotedPost(embed: unknown): embed is AppBskyEmbedRecord.View { - return Boolean( - embed && - AppBskyEmbedRecord.isView(embed) && - AppBskyEmbedRecord.isViewRecord(embed.record) && - AppBskyFeedPost.isRecord(embed.record.value) && - AppBskyFeedPost.validateRecord(embed.record.value).success, - ) + return Boolean(embed && AppBskyEmbedRecord.isView(embed)) } export function isQuotedPostWithMedia( embed: unknown, ): embed is AppBskyEmbedRecordWithMedia.View { - return Boolean( - embed && - AppBskyEmbedRecordWithMedia.isView(embed) && - AppBskyEmbedRecord.isViewRecord(embed.record.record) && - AppBskyFeedPost.isRecord(embed.record.record.value) && - AppBskyFeedPost.validateRecord(embed.record.record.value).success, - ) + return Boolean(embed && AppBskyEmbedRecordWithMedia.isView(embed)) } export function toModerationUI(decision: ModerationDecision): ModerationUI { diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 400bc76a24a..956a481627c 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -4283,22 +4283,34 @@ export const schemaDict = { }, viewNotFound: { type: 'object', - required: ['uri'], + required: ['uri', 'notFound'], properties: { uri: { type: 'string', format: 'at-uri', }, + notFound: { + type: 'boolean', + const: true, + }, }, }, viewBlocked: { type: 'object', - required: ['uri'], + 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', + }, }, }, }, diff --git a/packages/bsky/src/lexicon/types/app/bsky/embed/record.ts b/packages/bsky/src/lexicon/types/app/bsky/embed/record.ts index cdf38d6d7ad..cea5742a45e 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/embed/record.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/embed/record.ts @@ -84,6 +84,7 @@ export function validateViewRecord(v: unknown): ValidationResult { export interface ViewNotFound { uri: string + notFound: true [k: string]: unknown } @@ -101,6 +102,8 @@ export function validateViewNotFound(v: unknown): ValidationResult { export interface ViewBlocked { uri: string + blocked: true + author: AppBskyFeedDefs.BlockedAuthor [k: string]: unknown } diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index 0d71be81aa9..be8983837c7 100644 --- a/packages/bsky/src/services/feed/index.ts +++ b/packages/bsky/src/services/feed/index.ts @@ -627,6 +627,7 @@ export class FeedService { recordEmbedViews[uri] = { $type: 'app.bsky.embed.record#viewNotFound', uri, + notFound: true, } } } @@ -701,6 +702,16 @@ function applyEmbedBlock( return { $type: 'app.bsky.embed.record#viewBlocked', uri: view.uri, + blocked: true, + author: { + did: view.author.did, + viewer: view.author.viewer + ? { + blockedBy: view.author.viewer?.blockedBy, + blocking: view.author.viewer?.blocking, + } + : undefined, + }, } } return view diff --git a/packages/bsky/src/services/feed/views.ts b/packages/bsky/src/services/feed/views.ts index 6bef3e9182e..0f24b6e8f2e 100644 --- a/packages/bsky/src/services/feed/views.ts +++ b/packages/bsky/src/services/feed/views.ts @@ -291,12 +291,23 @@ export class FeedViews { return { $type: 'app.bsky.embed.record#viewNotFound', uri, + notFound: true, } } if (post.author.viewer?.blocking || post.author.viewer?.blockedBy) { return { $type: 'app.bsky.embed.record#viewBlocked', uri, + blocked: true, + author: { + did: post.author.did, + viewer: post.author.viewer + ? { + blockedBy: post.author.viewer?.blockedBy, + blocking: post.author.viewer?.blocking, + } + : undefined, + }, } } return { diff --git a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap index 1c5206c28de..f420c6950fe 100644 --- a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap +++ b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap @@ -18,6 +18,7 @@ Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewNotFound", + "notFound": true, "uri": "record(1)", }, }, diff --git a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap index d54a3e7270d..086f6e10d4d 100644 --- a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap @@ -54,6 +54,14 @@ Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewBlocked", + "author": Object { + "did": "user(3)", + "viewer": Object { + "blockedBy": false, + "blocking": "record(5)", + }, + }, + "blocked": true, "uri": "record(4)", }, }, diff --git a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap index cee906e2b25..417015173d3 100644 --- a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap @@ -167,6 +167,7 @@ Array [ "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewNotFound", + "notFound": true, "uri": "record(7)", }, }, @@ -294,6 +295,7 @@ Array [ "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewNotFound", + "notFound": true, "uri": "record(7)", }, }, @@ -703,6 +705,7 @@ Array [ "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewNotFound", + "notFound": true, "uri": "record(9)", }, }, diff --git a/packages/pds/src/app-view/services/feed/index.ts b/packages/pds/src/app-view/services/feed/index.ts index f835d7cac51..a028bed27f6 100644 --- a/packages/pds/src/app-view/services/feed/index.ts +++ b/packages/pds/src/app-view/services/feed/index.ts @@ -582,6 +582,7 @@ export class FeedService { recordEmbedViews[uri] = { $type: 'app.bsky.embed.record#viewNotFound', uri, + notFound: true, } } } @@ -668,6 +669,16 @@ function applyEmbedBlock( return { $type: 'app.bsky.embed.record#viewBlocked', uri: view.uri, + blocked: true, + author: { + did: view.author.did, + viewer: view.author.viewer + ? { + blockedBy: view.author.viewer?.blockedBy, + blocking: view.author.viewer?.blocking, + } + : undefined, + }, } } return view diff --git a/packages/pds/src/app-view/services/feed/views.ts b/packages/pds/src/app-view/services/feed/views.ts index d1859fc7d1b..1897542a310 100644 --- a/packages/pds/src/app-view/services/feed/views.ts +++ b/packages/pds/src/app-view/services/feed/views.ts @@ -289,12 +289,23 @@ export class FeedViews { return { $type: 'app.bsky.embed.record#viewNotFound', uri, + notFound: true, } } if (post.author.viewer?.blocking || post.author.viewer?.blockedBy) { return { $type: 'app.bsky.embed.record#viewBlocked', uri, + blocked: true, + author: { + did: post.author.did, + viewer: post.author.viewer + ? { + blockedBy: post.author.viewer?.blockedBy, + blocking: post.author.viewer?.blocking, + } + : undefined, + }, } } return { diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 400bc76a24a..956a481627c 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -4283,22 +4283,34 @@ export const schemaDict = { }, viewNotFound: { type: 'object', - required: ['uri'], + required: ['uri', 'notFound'], properties: { uri: { type: 'string', format: 'at-uri', }, + notFound: { + type: 'boolean', + const: true, + }, }, }, viewBlocked: { type: 'object', - required: ['uri'], + 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', + }, }, }, }, diff --git a/packages/pds/src/lexicon/types/app/bsky/embed/record.ts b/packages/pds/src/lexicon/types/app/bsky/embed/record.ts index cdf38d6d7ad..cea5742a45e 100644 --- a/packages/pds/src/lexicon/types/app/bsky/embed/record.ts +++ b/packages/pds/src/lexicon/types/app/bsky/embed/record.ts @@ -84,6 +84,7 @@ export function validateViewRecord(v: unknown): ValidationResult { export interface ViewNotFound { uri: string + notFound: true [k: string]: unknown } @@ -101,6 +102,8 @@ export function validateViewNotFound(v: unknown): ValidationResult { export interface ViewBlocked { uri: string + blocked: true + author: AppBskyFeedDefs.BlockedAuthor [k: string]: unknown } diff --git a/packages/pds/tests/__snapshots__/feed-generation.test.ts.snap b/packages/pds/tests/__snapshots__/feed-generation.test.ts.snap index 93bc44a6f17..b1c5319fd7d 100644 --- a/packages/pds/tests/__snapshots__/feed-generation.test.ts.snap +++ b/packages/pds/tests/__snapshots__/feed-generation.test.ts.snap @@ -35,6 +35,7 @@ Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewNotFound", + "notFound": true, "uri": "record(2)", }, }, diff --git a/packages/pds/tests/views/__snapshots__/blocks.test.ts.snap b/packages/pds/tests/views/__snapshots__/blocks.test.ts.snap index adcf8f45b77..9b168ae33a5 100644 --- a/packages/pds/tests/views/__snapshots__/blocks.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/blocks.test.ts.snap @@ -62,6 +62,14 @@ Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewBlocked", + "author": Object { + "did": "user(2)", + "viewer": Object { + "blockedBy": false, + "blocking": "record(5)", + }, + }, + "blocked": true, "uri": "record(4)", }, }, diff --git a/packages/pds/tests/views/__snapshots__/timeline.test.ts.snap b/packages/pds/tests/views/__snapshots__/timeline.test.ts.snap index a818274c1a3..c200df7f35a 100644 --- a/packages/pds/tests/views/__snapshots__/timeline.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/timeline.test.ts.snap @@ -183,6 +183,7 @@ Array [ "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewNotFound", + "notFound": true, "uri": "record(7)", }, }, @@ -318,6 +319,7 @@ Array [ "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewNotFound", + "notFound": true, "uri": "record(7)", }, }, @@ -743,6 +745,7 @@ Array [ "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewNotFound", + "notFound": true, "uri": "record(9)", }, }, From 9a824aeae63eacde2f3b10eb276a84e16f92d6ff Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 10 Aug 2023 15:37:50 -0700 Subject: [PATCH 130/237] @atproto/api@0.6.3 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 8b637b2620a..f100155dbdb 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.2", + "version": "0.6.3", "main": "src/index.ts", "scripts": { "codegen": "yarn docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From cfb6835664f845d43718272fb3150b735499af79 Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Fri, 11 Aug 2023 15:46:42 -0500 Subject: [PATCH 131/237] Require bluesky-social/atproto for .github/workflows/build-and-push-* (#1460) --- .github/workflows/build-and-push-bsky-aws.yaml | 1 + .github/workflows/build-and-push-bsky-ghcr.yaml | 1 + .github/workflows/build-and-push-pds-aws.yaml | 1 + .github/workflows/build-and-push-pds-ghcr.yaml | 1 + 4 files changed, 4 insertions(+) diff --git a/.github/workflows/build-and-push-bsky-aws.yaml b/.github/workflows/build-and-push-bsky-aws.yaml index b656dec89c9..009b2bff01b 100644 --- a/.github/workflows/build-and-push-bsky-aws.yaml +++ b/.github/workflows/build-and-push-bsky-aws.yaml @@ -11,6 +11,7 @@ env: jobs: bsky-container-aws: + if: github.repository == 'bluesky-social/atproto' runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/build-and-push-bsky-ghcr.yaml b/.github/workflows/build-and-push-bsky-ghcr.yaml index 26f46e8c4db..f31284be315 100644 --- a/.github/workflows/build-and-push-bsky-ghcr.yaml +++ b/.github/workflows/build-and-push-bsky-ghcr.yaml @@ -13,6 +13,7 @@ env: jobs: bsky-container-ghcr: + if: github.repository == 'bluesky-social/atproto' runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/build-and-push-pds-aws.yaml b/.github/workflows/build-and-push-pds-aws.yaml index 2565b38e432..8b821682463 100644 --- a/.github/workflows/build-and-push-pds-aws.yaml +++ b/.github/workflows/build-and-push-pds-aws.yaml @@ -11,6 +11,7 @@ env: jobs: pds-container-aws: + if: github.repository == 'bluesky-social/atproto' runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/build-and-push-pds-ghcr.yaml b/.github/workflows/build-and-push-pds-ghcr.yaml index cc6bc8939c9..ef4036cdeb8 100644 --- a/.github/workflows/build-and-push-pds-ghcr.yaml +++ b/.github/workflows/build-and-push-pds-ghcr.yaml @@ -13,6 +13,7 @@ env: jobs: pds-container-ghcr: + if: github.repository == 'bluesky-social/atproto' runs-on: ubuntu-latest permissions: contents: read From b1571b405de0e61b4934d3812defe23b9ec2faac Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 9 Aug 2023 18:20:39 -0500 Subject: [PATCH 132/237] v0.1.12 --- packages/pds/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/package.json b/packages/pds/package.json index 4690a0ba665..b27ce107f4c 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/pds", - "version": "0.1.11", + "version": "0.1.12", "license": "MIT", "repository": { "type": "git", From c16fcb84bc0a91db956442d2cf00a03b0906dcb1 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 9 Aug 2023 18:21:22 -0500 Subject: [PATCH 133/237] v0.0.3 --- packages/bsky/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bsky/package.json b/packages/bsky/package.json index c16ec6c1d70..c2a5a756864 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/bsky", - "version": "0.0.2", + "version": "0.0.3", "license": "MIT", "repository": { "type": "git", From 5b355adbf8bcadfa28216533dafa4ee71a26c936 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Aug 2023 15:52:10 -0500 Subject: [PATCH 134/237] add getActorLikes lexicon --- lexicons/app/bsky/feed/getAuthorLikes.json | 37 ++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 lexicons/app/bsky/feed/getAuthorLikes.json diff --git a/lexicons/app/bsky/feed/getAuthorLikes.json b/lexicons/app/bsky/feed/getAuthorLikes.json new file mode 100644 index 00000000000..38bb353aaba --- /dev/null +++ b/lexicons/app/bsky/feed/getAuthorLikes.json @@ -0,0 +1,37 @@ +{ + "lexicon": 1, + "id": "app.bsky.feed.getActorLikes", + "defs": { + "main": { + "type": "query", + "description": "A view of the 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": "app.bsky.feed.defs#feedViewPost"} + } + } + } + }, + "errors": [ + {"name": "BlockedActor"}, + {"name": "BlockedByActor"} + ] + } + } +} From b05986f7d16589ae539582730fd028e9480754da Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 14 Aug 2023 15:39:48 -0500 Subject: [PATCH 135/237] codegen --- packages/api/src/client/index.ts | 13 +++++ packages/api/src/client/lexicons.ts | 57 +++++++++++++++++++ .../types/app/bsky/feed/getActorLikes.ts | 53 +++++++++++++++++ packages/bsky/src/lexicon/index.ts | 8 +++ packages/bsky/src/lexicon/lexicons.ts | 57 +++++++++++++++++++ .../types/app/bsky/feed/getActorLikes.ts | 47 +++++++++++++++ packages/pds/src/lexicon/index.ts | 8 +++ packages/pds/src/lexicon/lexicons.ts | 57 +++++++++++++++++++ .../types/app/bsky/feed/getActorLikes.ts | 47 +++++++++++++++ 9 files changed, 347 insertions(+) create mode 100644 packages/api/src/client/types/app/bsky/feed/getActorLikes.ts create mode 100644 packages/bsky/src/lexicon/types/app/bsky/feed/getActorLikes.ts create mode 100644 packages/pds/src/lexicon/types/app/bsky/feed/getActorLikes.ts diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 6429fc217c0..e4971107dac 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -90,6 +90,7 @@ import * as AppBskyFeedDescribeFeedGenerator from './types/app/bsky/feed/describ import * as AppBskyFeedGenerator from './types/app/bsky/feed/generator' import * as AppBskyFeedGetActorFeeds from './types/app/bsky/feed/getActorFeeds' import * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed' +import * as AppBskyFeedGetActorLikes from './types/app/bsky/feed/getActorLikes' 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' @@ -210,6 +211,7 @@ export * as AppBskyFeedDescribeFeedGenerator from './types/app/bsky/feed/describ export * as AppBskyFeedGenerator from './types/app/bsky/feed/generator' export * as AppBskyFeedGetActorFeeds from './types/app/bsky/feed/getActorFeeds' export * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed' +export * as AppBskyFeedGetActorLikes from './types/app/bsky/feed/getActorLikes' export * as AppBskyFeedGetFeed from './types/app/bsky/feed/getFeed' export * as AppBskyFeedGetFeedGenerator from './types/app/bsky/feed/getFeedGenerator' export * as AppBskyFeedGetFeedGenerators from './types/app/bsky/feed/getFeedGenerators' @@ -1257,6 +1259,17 @@ export class FeedNS { }) } + getActorLikes( + params?: AppBskyFeedGetActorLikes.QueryParams, + opts?: AppBskyFeedGetActorLikes.CallOptions, + ): Promise { + return this._service.xrpc + .call('app.bsky.feed.getActorLikes', params, undefined, opts) + .catch((e) => { + throw AppBskyFeedGetActorLikes.toKnownErr(e) + }) + } + getFeed( params?: AppBskyFeedGetFeed.QueryParams, opts?: AppBskyFeedGetFeed.CallOptions, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 956a481627c..045f5224f2c 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -4861,6 +4861,62 @@ export const schemaDict = { }, }, }, + AppBskyFeedGetActorLikes: { + lexicon: 1, + id: 'app.bsky.feed.getActorLikes', + defs: { + main: { + type: 'query', + description: 'A view of the 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', + }, + ], + }, + }, + }, AppBskyFeedGetFeed: { lexicon: 1, id: 'app.bsky.feed.getFeed', @@ -6638,6 +6694,7 @@ export const ids = { AppBskyFeedGenerator: 'app.bsky.feed.generator', AppBskyFeedGetActorFeeds: 'app.bsky.feed.getActorFeeds', AppBskyFeedGetAuthorFeed: 'app.bsky.feed.getAuthorFeed', + AppBskyFeedGetActorLikes: 'app.bsky.feed.getActorLikes', AppBskyFeedGetFeed: 'app.bsky.feed.getFeed', AppBskyFeedGetFeedGenerator: 'app.bsky.feed.getFeedGenerator', AppBskyFeedGetFeedGenerators: 'app.bsky.feed.getFeedGenerators', diff --git a/packages/api/src/client/types/app/bsky/feed/getActorLikes.ts b/packages/api/src/client/types/app/bsky/feed/getActorLikes.ts new file mode 100644 index 00000000000..0f101ca4c3b --- /dev/null +++ b/packages/api/src/client/types/app/bsky/feed/getActorLikes.ts @@ -0,0 +1,53 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as AppBskyFeedDefs from './defs' + +export interface QueryParams { + actor: string + limit?: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + feed: AppBskyFeedDefs.FeedViewPost[] + [k: string]: unknown +} + +export interface CallOptions { + headers?: Headers +} + +export interface Response { + success: boolean + headers: Headers + data: OutputSchema +} + +export class BlockedActorError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers) + } +} + +export class BlockedByActorError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers) + } +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + if (e.error === 'BlockedActor') return new BlockedActorError(e) + if (e.error === 'BlockedByActor') return new BlockedByActorError(e) + } + return e +} diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 17c90d9ec8d..57ccd8918a6 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -79,6 +79,7 @@ import * as AppBskyActorSearchActorsTypeahead from './types/app/bsky/actor/searc import * as AppBskyFeedDescribeFeedGenerator from './types/app/bsky/feed/describeFeedGenerator' import * as AppBskyFeedGetActorFeeds from './types/app/bsky/feed/getActorFeeds' import * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed' +import * as AppBskyFeedGetActorLikes from './types/app/bsky/feed/getActorLikes' 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' @@ -843,6 +844,13 @@ export class FeedNS { return this._server.xrpc.method(nsid, cfg) } + getActorLikes( + cfg: ConfigOf>>, + ) { + const nsid = 'app.bsky.feed.getActorLikes' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + getFeed( cfg: ConfigOf>>, ) { diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 956a481627c..045f5224f2c 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -4861,6 +4861,62 @@ export const schemaDict = { }, }, }, + AppBskyFeedGetActorLikes: { + lexicon: 1, + id: 'app.bsky.feed.getActorLikes', + defs: { + main: { + type: 'query', + description: 'A view of the 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', + }, + ], + }, + }, + }, AppBskyFeedGetFeed: { lexicon: 1, id: 'app.bsky.feed.getFeed', @@ -6638,6 +6694,7 @@ export const ids = { AppBskyFeedGenerator: 'app.bsky.feed.generator', AppBskyFeedGetActorFeeds: 'app.bsky.feed.getActorFeeds', AppBskyFeedGetAuthorFeed: 'app.bsky.feed.getAuthorFeed', + AppBskyFeedGetActorLikes: 'app.bsky.feed.getActorLikes', AppBskyFeedGetFeed: 'app.bsky.feed.getFeed', AppBskyFeedGetFeedGenerator: 'app.bsky.feed.getFeedGenerator', AppBskyFeedGetFeedGenerators: 'app.bsky.feed.getFeedGenerators', diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getActorLikes.ts new file mode 100644 index 00000000000..ce56c2667fd --- /dev/null +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getActorLikes.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 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 Handler = (ctx: { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +}) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 17c90d9ec8d..57ccd8918a6 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -79,6 +79,7 @@ import * as AppBskyActorSearchActorsTypeahead from './types/app/bsky/actor/searc import * as AppBskyFeedDescribeFeedGenerator from './types/app/bsky/feed/describeFeedGenerator' import * as AppBskyFeedGetActorFeeds from './types/app/bsky/feed/getActorFeeds' import * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed' +import * as AppBskyFeedGetActorLikes from './types/app/bsky/feed/getActorLikes' 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' @@ -843,6 +844,13 @@ export class FeedNS { return this._server.xrpc.method(nsid, cfg) } + getActorLikes( + cfg: ConfigOf>>, + ) { + const nsid = 'app.bsky.feed.getActorLikes' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + getFeed( cfg: ConfigOf>>, ) { diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 956a481627c..045f5224f2c 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -4861,6 +4861,62 @@ export const schemaDict = { }, }, }, + AppBskyFeedGetActorLikes: { + lexicon: 1, + id: 'app.bsky.feed.getActorLikes', + defs: { + main: { + type: 'query', + description: 'A view of the 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', + }, + ], + }, + }, + }, AppBskyFeedGetFeed: { lexicon: 1, id: 'app.bsky.feed.getFeed', @@ -6638,6 +6694,7 @@ export const ids = { AppBskyFeedGenerator: 'app.bsky.feed.generator', AppBskyFeedGetActorFeeds: 'app.bsky.feed.getActorFeeds', AppBskyFeedGetAuthorFeed: 'app.bsky.feed.getAuthorFeed', + AppBskyFeedGetActorLikes: 'app.bsky.feed.getActorLikes', AppBskyFeedGetFeed: 'app.bsky.feed.getFeed', AppBskyFeedGetFeedGenerator: 'app.bsky.feed.getFeedGenerator', AppBskyFeedGetFeedGenerators: 'app.bsky.feed.getFeedGenerators', diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getActorLikes.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getActorLikes.ts new file mode 100644 index 00000000000..ce56c2667fd --- /dev/null +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getActorLikes.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 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 Handler = (ctx: { + auth: HA + params: QueryParams + input: HandlerInput + req: express.Request + res: express.Response +}) => Promise | HandlerOutput From 73f64009a22b3428a21a6d7807aee89d629fd0a4 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 14 Aug 2023 17:38:00 -0500 Subject: [PATCH 136/237] Appview database replicas (#1459) * setup bsky so that writes always go to a db primary * test bsky primary db functionality * sprinkle db primary over moderation and view maintainer usage * make bsky tests more strict on primary/secondary db, fixes * maintain prev db config on appview, require primary db, make primary/replica explicit in config * tidy * sketch out db replicas * wip * support multiple tags per bsky replica, "any" tag, fallbacks and warns * clarify tags * use new coordinator api across routes & services * wire-up replica tagging config * tidy * cleanup entroypoints and tests for db coordinator * fix pds test * re-hookup migrate db * standardize env vars for db on bsky services * tidy * build * fix bav api entrypoint * support DB_POSTGRES_URL on ingester/indexer for backwards compat * remove build --------- Co-authored-by: Devin Ivy --- packages/bsky/build.js | 2 +- packages/bsky/service/api.js | 80 ++++-- packages/bsky/service/indexer.js | 23 +- packages/bsky/service/ingester.js | 9 +- .../bsky/src/api/app/bsky/actor/getProfile.ts | 4 +- .../src/api/app/bsky/actor/getProfiles.ts | 4 +- .../src/api/app/bsky/actor/getSuggestions.ts | 12 +- .../src/api/app/bsky/actor/searchActors.ts | 5 +- .../app/bsky/actor/searchActorsTypeahead.ts | 5 +- .../src/api/app/bsky/feed/getActorFeeds.ts | 7 +- .../src/api/app/bsky/feed/getAuthorFeed.ts | 15 +- .../bsky/src/api/app/bsky/feed/getFeed.ts | 11 +- .../src/api/app/bsky/feed/getFeedGenerator.ts | 3 +- .../api/app/bsky/feed/getFeedGenerators.ts | 3 +- .../bsky/src/api/app/bsky/feed/getLikes.ts | 5 +- .../src/api/app/bsky/feed/getPostThread.ts | 17 +- .../bsky/src/api/app/bsky/feed/getPosts.ts | 3 +- .../src/api/app/bsky/feed/getRepostedBy.ts | 4 +- .../bsky/src/api/app/bsky/feed/getTimeline.ts | 9 +- .../bsky/src/api/app/bsky/graph/getBlocks.ts | 6 +- .../src/api/app/bsky/graph/getFollowers.ts | 8 +- .../bsky/src/api/app/bsky/graph/getFollows.ts | 8 +- .../bsky/src/api/app/bsky/graph/getList.ts | 6 +- .../src/api/app/bsky/graph/getListMutes.ts | 8 +- .../bsky/src/api/app/bsky/graph/getLists.ts | 6 +- .../bsky/src/api/app/bsky/graph/getMutes.ts | 8 +- .../bsky/src/api/app/bsky/graph/muteActor.ts | 6 +- .../src/api/app/bsky/graph/muteActorList.ts | 4 +- .../src/api/app/bsky/graph/unmuteActor.ts | 6 +- .../src/api/app/bsky/graph/unmuteActorList.ts | 3 +- .../app/bsky/notification/getUnreadCount.ts | 5 +- .../bsky/notification/listNotifications.ts | 15 +- .../api/app/bsky/notification/updateSeen.ts | 6 +- .../unspecced/getPopularFeedGenerators.ts | 12 +- .../app/bsky/unspecced/getTimelineSkeleton.ts | 4 +- packages/bsky/src/api/blob-resolver.ts | 15 +- .../com/atproto/admin/getModerationAction.ts | 4 +- .../com/atproto/admin/getModerationActions.ts | 4 +- .../com/atproto/admin/getModerationReport.ts | 4 +- .../com/atproto/admin/getModerationReports.ts | 4 +- .../src/api/com/atproto/admin/getRecord.ts | 4 +- .../bsky/src/api/com/atproto/admin/getRepo.ts | 6 +- .../atproto/admin/resolveModerationReports.ts | 6 +- .../atproto/admin/reverseModerationAction.ts | 8 +- .../src/api/com/atproto/admin/searchRepos.ts | 9 +- .../com/atproto/admin/takeModerationAction.ts | 8 +- .../api/com/atproto/identity/resolveHandle.ts | 3 +- .../com/atproto/moderation/createReport.ts | 7 +- .../src/api/com/atproto/repo/getRecord.ts | 5 +- packages/bsky/src/api/health.ts | 3 +- packages/bsky/src/background.ts | 6 +- packages/bsky/src/bin.ts | 25 -- packages/bsky/src/config.ts | 47 +++- packages/bsky/src/context.ts | 6 +- packages/bsky/src/db/coordinator.ts | 107 ++++++++ packages/bsky/src/db/db.ts | 85 +++++++ packages/bsky/src/db/index.ts | 236 +----------------- packages/bsky/src/db/leader.ts | 6 +- packages/bsky/src/db/primary.ts | 184 ++++++++++++++ packages/bsky/src/db/types.ts | 10 + packages/bsky/src/db/views.ts | 4 +- packages/bsky/src/did-cache.ts | 6 +- packages/bsky/src/feed-gen/best-of-follows.ts | 7 +- packages/bsky/src/feed-gen/bsky-team.ts | 9 +- packages/bsky/src/feed-gen/hot-classic.ts | 9 +- packages/bsky/src/feed-gen/mutuals.ts | 9 +- packages/bsky/src/feed-gen/whats-hot.ts | 7 +- packages/bsky/src/feed-gen/with-friends.ts | 5 +- packages/bsky/src/index.ts | 33 ++- packages/bsky/src/indexer/config.ts | 2 +- packages/bsky/src/indexer/context.ts | 6 +- packages/bsky/src/indexer/index.ts | 6 +- packages/bsky/src/indexer/services.ts | 8 +- packages/bsky/src/ingester/config.ts | 2 +- packages/bsky/src/ingester/context.ts | 6 +- packages/bsky/src/ingester/index.ts | 6 +- packages/bsky/src/label-cache.ts | 4 +- packages/bsky/src/labeler/base.ts | 4 +- packages/bsky/src/labeler/hive.ts | 11 +- packages/bsky/src/labeler/keyword.ts | 4 +- packages/bsky/src/services/actor/index.ts | 2 +- packages/bsky/src/services/actor/views.ts | 2 +- packages/bsky/src/services/feed/index.ts | 2 +- packages/bsky/src/services/feed/views.ts | 2 +- packages/bsky/src/services/graph/index.ts | 22 +- packages/bsky/src/services/index.ts | 6 +- packages/bsky/src/services/indexing/index.ts | 8 +- .../src/services/indexing/plugins/block.ts | 4 +- .../indexing/plugins/feed-generator.ts | 4 +- .../src/services/indexing/plugins/follow.ts | 4 +- .../src/services/indexing/plugins/like.ts | 4 +- .../services/indexing/plugins/list-item.ts | 4 +- .../src/services/indexing/plugins/list.ts | 4 +- .../src/services/indexing/plugins/post.ts | 4 +- .../src/services/indexing/plugins/profile.ts | 4 +- .../src/services/indexing/plugins/repost.ts | 4 +- .../bsky/src/services/indexing/processor.ts | 6 +- packages/bsky/src/services/label/index.ts | 7 +- .../bsky/src/services/moderation/index.ts | 6 +- .../bsky/src/services/moderation/views.ts | 2 +- packages/bsky/src/services/util/search.ts | 2 +- packages/bsky/tests/algos/whats-hot.test.ts | 9 +- packages/bsky/tests/db.test.ts | 5 +- packages/bsky/tests/did-cache.test.ts | 4 +- packages/bsky/tests/duplicate-records.test.ts | 3 +- .../bsky/tests/handle-invalidation.test.ts | 5 +- packages/bsky/tests/indexing.test.ts | 1 - packages/bsky/tests/moderation.test.ts | 8 +- packages/bsky/tests/server.test.ts | 2 +- packages/bsky/tests/subscription/repo.test.ts | 4 +- .../bsky/tests/views/actor-search.test.ts | 2 +- .../tests/views/admin/repo-search.test.ts | 2 +- packages/bsky/tests/views/suggestions.test.ts | 5 +- packages/bsky/tests/views/timeline.test.ts | 4 +- packages/dev-env/src/bsky.ts | 31 ++- packages/dev-env/src/network.ts | 2 +- packages/dev-env/src/types.ts | 2 +- packages/pds/tests/proxied/views.test.ts | 5 +- 118 files changed, 881 insertions(+), 598 deletions(-) delete mode 100644 packages/bsky/src/bin.ts create mode 100644 packages/bsky/src/db/coordinator.ts create mode 100644 packages/bsky/src/db/db.ts create mode 100644 packages/bsky/src/db/primary.ts create mode 100644 packages/bsky/src/db/types.ts diff --git a/packages/bsky/build.js b/packages/bsky/build.js index ba40c492406..f27a88ec0cf 100644 --- a/packages/bsky/build.js +++ b/packages/bsky/build.js @@ -13,7 +13,7 @@ if (process.argv.includes('--update-main-to-dist')) { require('esbuild').build({ logLevel: 'info', - entryPoints: ['src/index.ts', 'src/bin.ts', 'src/db/index.ts'], + entryPoints: ['src/index.ts', 'src/db/index.ts'], bundle: true, sourcemap: true, outdir: 'dist', diff --git a/packages/bsky/service/api.js b/packages/bsky/service/api.js index e27221526d7..ab6b1a8d25e 100644 --- a/packages/bsky/service/api.js +++ b/packages/bsky/service/api.js @@ -12,9 +12,11 @@ require('dd-trace') // Only works with commonjs // Tracer code above must come before anything else const path = require('path') +const assert = require('assert') const { CloudfrontInvalidator } = require('@atproto/aws') const { - Database, + DatabaseCoordinator, + PrimaryDatabase, ServerConfig, BskyAppView, ViewMaintainer, @@ -23,25 +25,31 @@ const { const main = async () => { const env = getEnv() - // Migrate using credentialed user - const migrateDb = Database.postgres({ - url: env.dbMigratePostgresUrl, - schema: env.dbPostgresSchema, - poolSize: 2, - }) - await migrateDb.migrateToLatestOrThrow() - // Use lower-credentialed user to run the app - const db = Database.postgres({ - url: env.dbPostgresUrl, + assert(env.dbPrimaryPostgresUrl, 'missing configuration for db') + const db = new DatabaseCoordinator({ schema: env.dbPostgresSchema, - poolSize: env.dbPoolSize, - poolMaxUses: env.dbPoolMaxUses, - poolIdleTimeoutMs: env.dbPoolIdleTimeoutMs, + primary: { + url: env.dbPrimaryPostgresUrl, + poolSize: env.dbPrimaryPoolSize || env.dbPoolSize, + poolMaxUses: env.dbPoolMaxUses, + poolIdleTimeoutMs: env.dbPoolIdleTimeoutMs, + }, + replicas: env.dbReplicaPostgresUrls?.map((url, i) => { + return { + url, + poolSize: env.dbPoolSize, + poolMaxUses: env.dbPoolMaxUses, + poolIdleTimeoutMs: env.dbPoolIdleTimeoutMs, + tags: getTagsForIdx(env.dbReplicaTags, i), + } + }), }) const cfg = ServerConfig.readEnv({ port: env.port, version: env.version, - dbPostgresUrl: env.dbPostgresUrl, + dbPrimaryPostgresUrl: env.dbPrimaryPostgresUrl, + dbReplicaPostgresUrls: env.dbReplicaPostgresUrls, + dbReplicaTags: env.dbReplicaTags, dbPostgresSchema: env.dbPostgresSchema, publicUrl: env.publicUrl, didPlcUrl: env.didPlcUrl, @@ -63,6 +71,12 @@ const main = async () => { imgInvalidator: cfInvalidator, algos, }) + // separate db needed for more permissions + const migrateDb = new PrimaryDatabase({ + url: env.dbMigratePostgresUrl, + schema: env.dbPostgresSchema, + poolSize: 2, + }) const viewMaintainer = new ViewMaintainer(migrateDb) const viewMaintainerRunning = viewMaintainer.run() await bsky.start() @@ -78,9 +92,20 @@ const main = async () => { const getEnv = () => ({ port: parseInt(process.env.PORT), version: process.env.BSKY_VERSION, - dbPostgresUrl: process.env.DB_POSTGRES_URL, dbMigratePostgresUrl: - process.env.DB_MIGRATE_POSTGRES_URL || process.env.DB_POSTGRES_URL, + process.env.DB_MIGRATE_POSTGRES_URL || process.env.DB_PRIMARY_POSTGRES_URL, + dbPrimaryPostgresUrl: process.env.DB_PRIMARY_POSTGRES_URL, + dbPrimaryPoolSize: maybeParseInt(process.env.DB_PRIMARY_POOL_SIZE), + dbReplicaPostgresUrls: process.env.DB_REPLICA_POSTGRES_URLS + ? process.env.DB_REPLICA_POSTGRES_URLS.split(',') + : undefined, + dbReplicaTags: { + '*': getTagIdxs(process.env.DB_REPLICA_TAGS_ANY), // e.g. DB_REPLICA_TAGS_ANY=0,1 + timeline: getTagIdxs(process.env.DB_REPLICA_TAGS_TIMELINE), + feed: getTagIdxs(process.env.DB_REPLICA_TAGS_FEED), + search: getTagIdxs(process.env.DB_REPLICA_TAGS_SEARCH), + thread: getTagIdxs(process.env.DB_REPLICA_TAGS_THREAD), + }, dbPostgresSchema: process.env.DB_POSTGRES_SCHEMA || undefined, dbPoolSize: maybeParseInt(process.env.DB_POOL_SIZE), dbPoolMaxUses: maybeParseInt(process.env.DB_POOL_MAX_USES), @@ -95,6 +120,27 @@ const getEnv = () => ({ feedPublisherDid: process.env.FEED_PUBLISHER_DID, }) +/** + * @param {Record} tags + * @param {number} idx + */ +const getTagsForIdx = (tagMap, idx) => { + const tags = [] + for (const [tag, indexes] of Object.entries(tagMap)) { + if (indexes.includes(idx)) { + tags.push(tag) + } + } + return tags +} + +/** + * @param {string} str + */ +const getTagIdxs = (str) => { + return str ? str.split(',').map((item) => parseInt(item, 10)) : [] +} + const maybeParseInt = (str) => { const parsed = parseInt(str) return isNaN(parsed) ? undefined : parsed diff --git a/packages/bsky/service/indexer.js b/packages/bsky/service/indexer.js index f7071dd9de8..a2e28dcb114 100644 --- a/packages/bsky/service/indexer.js +++ b/packages/bsky/service/indexer.js @@ -3,20 +3,16 @@ require('dd-trace/init') // Only works with commonjs // Tracer code above must come before anything else -const { Database, IndexerConfig, BskyIndexer, Redis } = require('@atproto/bsky') +const { + IndexerConfig, + BskyIndexer, + Redis, + PrimaryDatabase, +} = require('@atproto/bsky') const main = async () => { const env = getEnv() - // Migrate using credentialed user - // @TODO temporarily disabled for testing purposes - // const migrateDb = Database.postgres({ - // url: env.dbMigratePostgresUrl, - // schema: env.dbPostgresSchema, - // poolSize: 2, - // }) - // await migrateDb.migrateToLatestOrThrow() - // await migrateDb.close() - const db = Database.postgres({ + const db = new PrimaryDatabase({ url: env.dbPostgresUrl, schema: env.dbPostgresSchema, poolSize: env.dbPoolSize, @@ -63,9 +59,8 @@ const main = async () => { // - INDEXER_SUB_LOCK_ID const getEnv = () => ({ version: process.env.BSKY_VERSION, - dbPostgresUrl: process.env.DB_POSTGRES_URL, - dbMigratePostgresUrl: - process.env.DB_MIGRATE_POSTGRES_URL || process.env.DB_POSTGRES_URL, + dbPostgresUrl: + process.env.DB_PRIMARY_POSTGRES_URL || process.env.DB_POSTGRES_URL, dbPostgresSchema: process.env.DB_POSTGRES_SCHEMA || undefined, dbPoolSize: maybeParseInt(process.env.DB_POOL_SIZE), dbPoolMaxUses: maybeParseInt(process.env.DB_POOL_MAX_USES), diff --git a/packages/bsky/service/ingester.js b/packages/bsky/service/ingester.js index 35d7ecd9979..19c33ea1067 100644 --- a/packages/bsky/service/ingester.js +++ b/packages/bsky/service/ingester.js @@ -4,7 +4,7 @@ require('dd-trace/init') // Only works with commonjs // Tracer code above must come before anything else const { - Database, + PrimaryDatabase, IngesterConfig, BskyIngester, Redis, @@ -13,7 +13,7 @@ const { const main = async () => { const env = getEnv() // No migration: ingester only uses pg for a lock - const db = Database.postgres({ + const db = new PrimaryDatabase({ url: env.dbPostgresUrl, schema: env.dbPostgresSchema, poolSize: env.dbPoolSize, @@ -59,9 +59,8 @@ const main = async () => { // - INGESTER_SUB_LOCK_ID const getEnv = () => ({ version: process.env.BSKY_VERSION, - dbPostgresUrl: process.env.DB_POSTGRES_URL, - dbMigratePostgresUrl: - process.env.DB_MIGRATE_POSTGRES_URL || process.env.DB_POSTGRES_URL, + dbPostgresUrl: + process.env.DB_PRIMARY_POSTGRES_URL || process.env.DB_POSTGRES_URL, dbPostgresSchema: process.env.DB_POSTGRES_SCHEMA || undefined, dbPoolSize: maybeParseInt(process.env.DB_POOL_SIZE), dbPoolMaxUses: maybeParseInt(process.env.DB_POOL_MAX_USES), diff --git a/packages/bsky/src/api/app/bsky/actor/getProfile.ts b/packages/bsky/src/api/app/bsky/actor/getProfile.ts index 21239a2eb86..8a7bfbe1abc 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfile.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfile.ts @@ -10,8 +10,8 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ auth, params, res }) => { const { actor } = params const requester = auth.credentials.did - const { db, services } = ctx - const actorService = services.actor(db) + const db = ctx.db.getReplica() + const actorService = ctx.services.actor(db) const [actorRes, repoRev] = await Promise.all([ actorService.getActor(actor, true), diff --git a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts index fb3f1fb518c..0fd7cdf844c 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts @@ -8,8 +8,8 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ auth, params, res }) => { const { actors } = params const requester = auth.credentials.did - const { db, services } = ctx - const actorService = services.actor(db) + const db = ctx.db.getReplica() + const actorService = ctx.services.actor(db) const [actorsRes, repoRev] = await Promise.all([ actorService.getActors(actors), diff --git a/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts b/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts index ba51957c36f..6a142519965 100644 --- a/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts +++ b/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts @@ -9,13 +9,13 @@ export default function (server: Server, ctx: AppContext) { const { limit, cursor } = params const viewer = auth.credentials.did - const actorService = ctx.services.actor(ctx.db) - const graphService = ctx.services.graph(ctx.db) + const db = ctx.db.getReplica() + const actorService = ctx.services.actor(db) + const graphService = ctx.services.graph(db) - const db = ctx.db.db - const { ref } = db.dynamic + const { ref } = db.db.dynamic - let suggestionsQb = db + let suggestionsQb = db.db .selectFrom('suggested_follow') .innerJoin('actor', 'actor.did', 'suggested_follow.did') .innerJoin('profile_agg', 'profile_agg.did', 'actor.did') @@ -35,7 +35,7 @@ export default function (server: Server, ctx: AppContext) { .orderBy('suggested_follow.order', 'asc') if (cursor) { - const cursorRow = await db + const cursorRow = await db.db .selectFrom('suggested_follow') .where('did', '=', cursor) .selectAll() diff --git a/packages/bsky/src/api/app/bsky/actor/searchActors.ts b/packages/bsky/src/api/app/bsky/actor/searchActors.ts index 9adc0b051db..9f462608885 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActors.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActors.ts @@ -11,11 +11,12 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.searchActors({ auth: ctx.authOptionalVerifier, handler: async ({ auth, params }) => { - const { services, db } = ctx const { cursor, limit, term: rawTerm } = params const requester = auth.credentials.did const term = cleanTerm(rawTerm || '') + const db = ctx.db.getReplica('search') + const results = term ? await getUserSearchQuery(db, { term, limit, cursor }) .select('distance') @@ -24,7 +25,7 @@ export default function (server: Server, ctx: AppContext) { : [] const keyset = new SearchKeyset(sql``, sql``) - const actors = await services + const actors = await ctx.services .actor(db) .views.hydrateProfiles(results, requester) const filtered = actors.filter( diff --git a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts index a20ecd1637f..b8533073be5 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -9,18 +9,19 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.searchActorsTypeahead({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth }) => { - const { services, db } = ctx const { limit, term: rawTerm } = params const requester = auth.credentials.did const term = cleanTerm(rawTerm || '') + const db = ctx.db.getReplica('search') + const results = term ? await getUserSearchQuerySimple(db, { term, limit }) .selectAll('actor') .execute() : [] - const actors = await services + const actors = await ctx.services .actor(db) .views.hydrateProfilesBasic(results, requester) diff --git a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts index a255bc944b0..a9e02d2cd59 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts @@ -10,15 +10,16 @@ export default function (server: Server, ctx: AppContext) { const { actor, limit, cursor } = params const viewer = auth.credentials.did - const actorService = ctx.services.actor(ctx.db) - const feedService = ctx.services.feed(ctx.db) + const db = ctx.db.getReplica() + const actorService = ctx.services.actor(db) + const feedService = ctx.services.feed(db) const creatorRes = await actorService.getActor(actor) if (!creatorRes) { throw new InvalidRequestError(`Actor not found: ${actor}`) } - const { ref } = ctx.db.db.dynamic + const { ref } = db.db.dynamic let feedsQb = feedService .selectFeedGeneratorQb(viewer) .where('feed_generator.creator', '=', creatorRes.did) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index b71cafbec02..e44f78cc373 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -11,12 +11,13 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth, res }) => { const { actor, limit, cursor, filter } = params const viewer = auth.credentials.did - const db = ctx.db.db - const { ref } = db.dynamic + + const db = ctx.db.getReplica() + const { ref } = db.db.dynamic // first verify there is not a block between requester & subject if (viewer !== null) { - const blocks = await ctx.services.graph(ctx.db).getBlocks(viewer, actor) + const blocks = await ctx.services.graph(db).getBlocks(viewer, actor) if (blocks.blocking) { throw new InvalidRequestError( `Requester has blocked actor: ${actor}`, @@ -30,15 +31,15 @@ export default function (server: Server, ctx: AppContext) { } } - const actorService = ctx.services.actor(ctx.db) - const feedService = ctx.services.feed(ctx.db) - const graphService = ctx.services.graph(ctx.db) + const actorService = ctx.services.actor(db) + const feedService = ctx.services.feed(db) + const graphService = ctx.services.graph(db) let did = '' if (actor.startsWith('did:')) { did = actor } else { - const actorRes = await db + const actorRes = await db.db .selectFrom('actor') .select('did') .where('handle', '=', actor) diff --git a/packages/bsky/src/api/app/bsky/feed/getFeed.ts b/packages/bsky/src/api/app/bsky/feed/getFeed.ts index ffed4a0e5a7..910ea514e94 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeed.ts @@ -16,6 +16,7 @@ import { OutputSchema as SkeletonOutput } from '../../../../lexicon/types/app/bs import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { AlgoResponse } from '../../../../feed-gen/types' +import { Database } from '../../../../db' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeed({ @@ -23,7 +24,9 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth, req }) => { const { feed } = params const viewer = auth.credentials.did - const feedService = ctx.services.feed(ctx.db) + + const db = ctx.db.getReplica() + const feedService = ctx.services.feed(db) const localAlgo = ctx.algos[feed] const timerSkele = new ServerTimer('skele').start() @@ -32,6 +35,7 @@ export default function (server: Server, ctx: AppContext) { ? await localAlgo(ctx, params, viewer) : await skeletonFromFeedGen( ctx, + db, params, viewer, req.headers['authorization'], @@ -58,13 +62,14 @@ export default function (server: Server, ctx: AppContext) { async function skeletonFromFeedGen( ctx: AppContext, + db: Database, params: GetFeedParams, viewer: string, authorization?: string, ): Promise { const { feed } = params // Resolve and fetch feed skeleton - const found = await ctx.db.db + const found = await db.db .selectFrom('feed_generator') .where('uri', '=', feed) .select('feedDid') @@ -126,7 +131,7 @@ async function skeletonFromFeedGen( const { feed: skeletonFeed, ...rest } = skeleton const cleanedFeed = await ctx.services - .feed(ctx.db) + .feed(db) .cleanFeedSkeleton(skeletonFeed, params.limit, viewer) return { diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts b/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts index c3eeb14f8c4..2f433e3f0db 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts @@ -14,7 +14,8 @@ export default function (server: Server, ctx: AppContext) { const { feed } = params const viewer = auth.credentials.did - const feedService = ctx.services.feed(ctx.db) + const db = ctx.db.getReplica() + const feedService = ctx.services.feed(db) const got = await feedService.getFeedGeneratorInfos([feed], viewer) const feedInfo = got[feed] diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts b/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts index f03f352a2eb..a81d962cb8b 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts @@ -8,7 +8,8 @@ export default function (server: Server, ctx: AppContext) { const { feeds } = params const requester = auth.credentials.did - const feedService = ctx.services.feed(ctx.db) + const db = ctx.db.getReplica() + const feedService = ctx.services.feed(db) const genInfos = await feedService.getFeedGeneratorInfos(feeds, requester) const genList = Object.values(genInfos) diff --git a/packages/bsky/src/api/app/bsky/feed/getLikes.ts b/packages/bsky/src/api/app/bsky/feed/getLikes.ts index 7a0d0551243..7a46beab9c2 100644 --- a/packages/bsky/src/api/app/bsky/feed/getLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getLikes.ts @@ -10,7 +10,8 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const { uri, limit, cursor, cid } = params const requester = auth.credentials.did - const { services, db } = ctx + + const db = ctx.db.getReplica() const { ref } = db.db.dynamic let builder = db.db @@ -38,7 +39,7 @@ export default function (server: Server, ctx: AppContext) { }) const likesRes = await builder.execute() - const actors = await services + const actors = await ctx.services .actor(db) .views.profiles(likesRes, requester) diff --git a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts index 677200bd387..95bc83e86c1 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts @@ -19,6 +19,7 @@ import { getAncestorsAndSelfQb, getDescendentsQb, } from '../../../../services/util/post' +import { Database } from '../../../../db' import { setRepoRev } from '../../../util' export type PostThread = { @@ -34,12 +35,13 @@ export default function (server: Server, ctx: AppContext) { const { uri, depth, parentHeight } = params const requester = auth.credentials.did - const actorService = ctx.services.actor(ctx.db) - const feedService = ctx.services.feed(ctx.db) - const labelService = ctx.services.label(ctx.db) + const db = ctx.db.getReplica('thread') + const actorService = ctx.services.actor(db) + const feedService = ctx.services.feed(db) + const labelService = ctx.services.label(db) const [threadData, repoRev] = await Promise.all([ - getThreadData(ctx, uri, depth, parentHeight), + getThreadData(ctx, db, uri, depth, parentHeight), actorService.getRepoRev(requester), ]) setRepoRev(res, repoRev) @@ -194,13 +196,14 @@ const getRelevantIds = ( const getThreadData = async ( ctx: AppContext, + db: Database, uri: string, depth: number, parentHeight: number, ): Promise => { - const feedService = ctx.services.feed(ctx.db) + const feedService = ctx.services.feed(db) const [parents, children] = await Promise.all([ - getAncestorsAndSelfQb(ctx.db.db, { uri, parentHeight }) + getAncestorsAndSelfQb(db.db, { uri, parentHeight }) .selectFrom('ancestor') .innerJoin( feedService.selectPostQb().as('post'), @@ -209,7 +212,7 @@ const getThreadData = async ( ) .selectAll('post') .execute(), - getDescendentsQb(ctx.db.db, { uri, depth }) + getDescendentsQb(db.db, { uri, depth }) .selectFrom('descendent') .innerJoin( feedService.selectPostQb().as('post'), diff --git a/packages/bsky/src/api/app/bsky/feed/getPosts.ts b/packages/bsky/src/api/app/bsky/feed/getPosts.ts index d964e3c5c57..4b092a5e717 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPosts.ts @@ -11,8 +11,9 @@ export default function (server: Server, ctx: AppContext) { const uris = common.dedupeStrs(params.uris) + const db = ctx.db.getReplica() const postViews = await ctx.services - .feed(ctx.db) + .feed(db) .getPostViews(uris, requester) const posts: PostView[] = [] diff --git a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts index ff635641163..9671dd14c70 100644 --- a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts @@ -9,7 +9,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const { uri, limit, cursor, cid } = params const requester = auth.credentials.did - const { services, db } = ctx + const db = ctx.db.getReplica() const { ref } = db.db.dynamic let builder = db.db @@ -32,7 +32,7 @@ export default function (server: Server, ctx: AppContext) { }) const repostedByRes = await builder.execute() - const repostedBy = await services + const repostedBy = await ctx.services .actor(db) .views.hydrateProfiles(repostedByRes, requester) diff --git a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts index 42713690829..55f0a2837c3 100644 --- a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts +++ b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts @@ -3,7 +3,7 @@ import { Server } from '../../../../lexicon' import { FeedAlgorithm, FeedKeyset, getFeedDateThreshold } from '../util/feed' import { paginate } from '../../../../db/pagination' import AppContext from '../../../../context' -import Database from '../../../../db' +import { Database } from '../../../../db' import { SkeletonFeedPost } from '../../../../lexicon/types/app/bsky/feed/defs' import { setRepoRev } from '../../../util' @@ -17,14 +17,15 @@ export default function (server: Server, ctx: AppContext) { if (algorithm && algorithm !== FeedAlgorithm.ReverseChronological) { throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`) } + const db = ctx.db.getReplica('timeline') const [skeleton, repoRev] = await Promise.all([ - getTimelineSkeleton(ctx.db, viewer, limit, cursor), - ctx.services.actor(ctx.db).getRepoRev(viewer), + getTimelineSkeleton(db, viewer, limit, cursor), + ctx.services.actor(db).getRepoRev(viewer), ]) setRepoRev(res, repoRev) - const feedService = ctx.services.feed(ctx.db) + const feedService = ctx.services.feed(db) const feedItems = await feedService.cleanFeedSkeleton( skeleton.feed, limit, diff --git a/packages/bsky/src/api/app/bsky/graph/getBlocks.ts b/packages/bsky/src/api/app/bsky/graph/getBlocks.ts index 45af81a7ce3..bb9c0fd2356 100644 --- a/packages/bsky/src/api/app/bsky/graph/getBlocks.ts +++ b/packages/bsky/src/api/app/bsky/graph/getBlocks.ts @@ -9,10 +9,10 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const { limit, cursor } = params const requester = auth.credentials.did - const { services, db } = ctx + const db = ctx.db.getReplica() const { ref } = db.db.dynamic - let blocksReq = ctx.db.db + let blocksReq = db.db .selectFrom('actor_block') .where('actor_block.creator', '=', requester) .innerJoin('actor as subject', 'subject.did', 'actor_block.subjectDid') @@ -32,7 +32,7 @@ export default function (server: Server, ctx: AppContext) { const blocksRes = await blocksReq.execute() - const actorService = services.actor(db) + const actorService = ctx.services.actor(db) const blocks = await actorService.views.hydrateProfiles( blocksRes, requester, diff --git a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts index eb73b14d56d..b10b4a0cd64 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts @@ -10,18 +10,18 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const { actor, limit, cursor } = params const requester = auth.credentials.did - const { services, db } = ctx + const db = ctx.db.getReplica() const { ref } = db.db.dynamic - const actorService = services.actor(db) - const graphService = services.graph(db) + const actorService = ctx.services.actor(db) + const graphService = ctx.services.graph(db) const subjectRes = await actorService.getActor(actor) if (!subjectRes) { throw new InvalidRequestError(`Actor not found: ${actor}`) } - let followersReq = ctx.db.db + let followersReq = db.db .selectFrom('follow') .where('follow.subjectDid', '=', subjectRes.did) .innerJoin('actor as creator', 'creator.did', 'follow.creator') diff --git a/packages/bsky/src/api/app/bsky/graph/getFollows.ts b/packages/bsky/src/api/app/bsky/graph/getFollows.ts index 35cb58e655b..e5b576d9804 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollows.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollows.ts @@ -10,18 +10,18 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const { actor, limit, cursor } = params const requester = auth.credentials.did - const { services, db } = ctx + const db = ctx.db.getReplica() const { ref } = db.db.dynamic - const actorService = services.actor(db) - const graphService = services.graph(db) + const actorService = ctx.services.actor(db) + const graphService = ctx.services.graph(db) const creatorRes = await actorService.getActor(actor) if (!creatorRes) { throw new InvalidRequestError(`Actor not found: ${actor}`) } - let followsReq = ctx.db.db + let followsReq = db.db .selectFrom('follow') .where('follow.creator', '=', creatorRes.did) .innerJoin('actor as subject', 'subject.did', 'follow.subjectDid') diff --git a/packages/bsky/src/api/app/bsky/graph/getList.ts b/packages/bsky/src/api/app/bsky/graph/getList.ts index ee0e9dc704d..068b35fb6df 100644 --- a/packages/bsky/src/api/app/bsky/graph/getList.ts +++ b/packages/bsky/src/api/app/bsky/graph/getList.ts @@ -9,10 +9,10 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const { list, limit, cursor } = params const requester = auth.credentials.did - const { services, db } = ctx + const db = ctx.db.getReplica() const { ref } = db.db.dynamic - const graphService = ctx.services.graph(ctx.db) + const graphService = ctx.services.graph(db) const listRes = await graphService .getListsQb(requester) @@ -38,7 +38,7 @@ export default function (server: Server, ctx: AppContext) { }) const itemsRes = await itemsReq.execute() - const actorService = services.actor(db) + const actorService = ctx.services.actor(db) const profiles = await actorService.views.hydrateProfiles( itemsRes, requester, diff --git a/packages/bsky/src/api/app/bsky/graph/getListMutes.ts b/packages/bsky/src/api/app/bsky/graph/getListMutes.ts index d85b86f9189..49d04b3233f 100644 --- a/packages/bsky/src/api/app/bsky/graph/getListMutes.ts +++ b/packages/bsky/src/api/app/bsky/graph/getListMutes.ts @@ -8,15 +8,15 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const { limit, cursor } = params const requester = auth.credentials.did - const { db } = ctx + const db = ctx.db.getReplica() const { ref } = db.db.dynamic - const graphService = ctx.services.graph(ctx.db) + const graphService = ctx.services.graph(db) let listsReq = graphService .getListsQb(requester) .whereExists( - ctx.db.db + db.db .selectFrom('list_mute') .where('list_mute.mutedByDid', '=', requester) .whereRef('list_mute.listUri', '=', ref('list.uri')) @@ -31,7 +31,7 @@ export default function (server: Server, ctx: AppContext) { }) const listsRes = await listsReq.execute() - const actorService = ctx.services.actor(ctx.db) + const actorService = ctx.services.actor(db) const profiles = await actorService.views.profiles(listsRes, requester) const lists = listsRes.map((row) => diff --git a/packages/bsky/src/api/app/bsky/graph/getLists.ts b/packages/bsky/src/api/app/bsky/graph/getLists.ts index 2f2d7878623..966bf0a594b 100644 --- a/packages/bsky/src/api/app/bsky/graph/getLists.ts +++ b/packages/bsky/src/api/app/bsky/graph/getLists.ts @@ -9,11 +9,11 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const { actor, limit, cursor } = params const requester = auth.credentials.did - const { services, db } = ctx + const db = ctx.db.getReplica() const { ref } = db.db.dynamic - const actorService = services.actor(db) - const graphService = services.graph(db) + const actorService = ctx.services.actor(db) + const graphService = ctx.services.graph(db) const creatorRes = await actorService.getActor(actor) if (!creatorRes) { diff --git a/packages/bsky/src/api/app/bsky/graph/getMutes.ts b/packages/bsky/src/api/app/bsky/graph/getMutes.ts index 0725089616a..0bac37edfdd 100644 --- a/packages/bsky/src/api/app/bsky/graph/getMutes.ts +++ b/packages/bsky/src/api/app/bsky/graph/getMutes.ts @@ -9,10 +9,10 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth }) => { const { limit, cursor } = params const requester = auth.credentials.did - const { services, db } = ctx - const { ref } = ctx.db.db.dynamic + const db = ctx.db.getReplica() + const { ref } = db.db.dynamic - let mutesReq = ctx.db.db + let mutesReq = db.db .selectFrom('mute') .innerJoin('actor', 'actor.did', 'mute.subjectDid') .where(notSoftDeletedClause(ref('actor'))) @@ -32,7 +32,7 @@ export default function (server: Server, ctx: AppContext) { const mutesRes = await mutesReq.execute() - const actorService = services.actor(db) + const actorService = ctx.services.actor(db) return { encoding: 'application/json', diff --git a/packages/bsky/src/api/app/bsky/graph/muteActor.ts b/packages/bsky/src/api/app/bsky/graph/muteActor.ts index 9489c7a6151..50a3723db6e 100644 --- a/packages/bsky/src/api/app/bsky/graph/muteActor.ts +++ b/packages/bsky/src/api/app/bsky/graph/muteActor.ts @@ -8,9 +8,9 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ auth, input }) => { const { actor } = input.body const requester = auth.credentials.did - const { db, services } = ctx + const db = ctx.db.getPrimary() - const subjectDid = await services.actor(db).getActorDid(actor) + const subjectDid = await ctx.services.actor(db).getActorDid(actor) if (!subjectDid) { throw new InvalidRequestError(`Actor not found: ${actor}`) } @@ -18,7 +18,7 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError('Cannot mute oneself') } - await services.graph(db).muteActor({ + await ctx.services.graph(db).muteActor({ subjectDid, mutedByDid: requester, }) diff --git a/packages/bsky/src/api/app/bsky/graph/muteActorList.ts b/packages/bsky/src/api/app/bsky/graph/muteActorList.ts index 3e35ef55bd9..bece0291c94 100644 --- a/packages/bsky/src/api/app/bsky/graph/muteActorList.ts +++ b/packages/bsky/src/api/app/bsky/graph/muteActorList.ts @@ -11,13 +11,15 @@ export default function (server: Server, ctx: AppContext) { const { list } = input.body const requester = auth.credentials.did + const db = ctx.db.getPrimary() + const listUri = new AtUri(list) const collId = lex.ids.AppBskyGraphList if (listUri.collection !== collId) { throw new InvalidRequestError(`Invalid collection: expected: ${collId}`) } - await ctx.services.graph(ctx.db).muteActorList({ + await ctx.services.graph(db).muteActorList({ list, mutedByDid: requester, }) diff --git a/packages/bsky/src/api/app/bsky/graph/unmuteActor.ts b/packages/bsky/src/api/app/bsky/graph/unmuteActor.ts index 93d485b1892..11af919126f 100644 --- a/packages/bsky/src/api/app/bsky/graph/unmuteActor.ts +++ b/packages/bsky/src/api/app/bsky/graph/unmuteActor.ts @@ -8,9 +8,9 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ auth, input }) => { const { actor } = input.body const requester = auth.credentials.did - const { db, services } = ctx + const db = ctx.db.getPrimary() - const subjectDid = await services.actor(db).getActorDid(actor) + const subjectDid = await ctx.services.actor(db).getActorDid(actor) if (!subjectDid) { throw new InvalidRequestError(`Actor not found: ${actor}`) } @@ -18,7 +18,7 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError('Cannot mute oneself') } - await services.graph(db).unmuteActor({ + await ctx.services.graph(db).unmuteActor({ subjectDid, mutedByDid: requester, }) diff --git a/packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts b/packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts index da63978fd23..8b97530c216 100644 --- a/packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts +++ b/packages/bsky/src/api/app/bsky/graph/unmuteActorList.ts @@ -7,8 +7,9 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ auth, input }) => { const { list } = input.body const requester = auth.credentials.did + const db = ctx.db.getPrimary() - await ctx.services.graph(ctx.db).unmuteActorList({ + await ctx.services.graph(db).unmuteActorList({ list, mutedByDid: requester, }) diff --git a/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts b/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts index 3c6b26e9a71..2d041b6ddeb 100644 --- a/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts @@ -12,8 +12,9 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError('The seenAt parameter is unsupported') } - const { ref } = ctx.db.db.dynamic - const result = await ctx.db.db + const db = ctx.db.getReplica() + const { ref } = db.db.dynamic + const result = await db.db .selectFrom('notification') .select(countAll.as('count')) .innerJoin('actor', 'actor.did', 'notification.did') diff --git a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts index 277a25f75cc..1f36207820e 100644 --- a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts @@ -17,10 +17,11 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError('The seenAt parameter is unsupported') } - const graphService = ctx.services.graph(ctx.db) + const db = ctx.db.getReplica() + const graphService = ctx.services.graph(db) - const { ref } = ctx.db.db.dynamic - let notifBuilder = ctx.db.db + const { ref } = db.db.dynamic + let notifBuilder = db.db .selectFrom('notification as notif') .innerJoin('record', 'record.uri', 'notif.recordUri') .innerJoin('actor as author', 'author.did', 'notif.author') @@ -35,7 +36,7 @@ export default function (server: Server, ctx: AppContext) { clause .where('reasonSubject', 'is', null) .orWhereExists( - ctx.db.db + db.db .selectFrom('record as subject') .selectAll() .whereRef('subject.uri', '=', ref('notif.reasonSubject')), @@ -64,7 +65,7 @@ export default function (server: Server, ctx: AppContext) { keyset, }) - const actorStateQuery = ctx.db.db + const actorStateQuery = db.db .selectFrom('actor_state') .selectAll() .where('did', '=', requester) @@ -76,8 +77,8 @@ export default function (server: Server, ctx: AppContext) { const seenAt = actorState?.lastSeenNotifs - const actorService = ctx.services.actor(ctx.db) - const labelService = ctx.services.label(ctx.db) + const actorService = ctx.services.actor(db) + const labelService = ctx.services.label(db) const recordUris = notifs.map((notif) => notif.uri) const [authors, labels] = await Promise.all([ actorService.views.profiles( diff --git a/packages/bsky/src/api/app/bsky/notification/updateSeen.ts b/packages/bsky/src/api/app/bsky/notification/updateSeen.ts index 98a53a8a585..b7c705c0889 100644 --- a/packages/bsky/src/api/app/bsky/notification/updateSeen.ts +++ b/packages/bsky/src/api/app/bsky/notification/updateSeen.ts @@ -17,12 +17,14 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError('Invalid date') } - await ctx.db.db + const db = ctx.db.getPrimary() + + await db.db .insertInto('actor_state') .values({ did: viewer, lastSeenNotifs: parsed }) .onConflict((oc) => oc.column('did').doUpdateSet({ - lastSeenNotifs: excluded(ctx.db.db, 'lastSeenNotifs'), + lastSeenNotifs: excluded(db.db, 'lastSeenNotifs'), }), ) .executeTakeFirst() diff --git a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index a29a21b687a..7c96522b6da 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -12,16 +12,16 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ auth, params }) => { const { limit, cursor, query } = params const requester = auth.credentials.did - const db = ctx.db.db - const { ref } = db.dynamic - const feedService = ctx.services.feed(ctx.db) + const db = ctx.db.getReplica() + const { ref } = db.db.dynamic + const feedService = ctx.services.feed(db) - let inner = ctx.db.db + let inner = db.db .selectFrom('feed_generator') .select([ 'uri', 'cid', - ctx.db.db + db.db .selectFrom('like') .whereRef('like.subject', '=', ref('feed_generator.uri')) .select(countAll.as('count')) @@ -36,7 +36,7 @@ export default function (server: Server, ctx: AppContext) { ) } - let builder = ctx.db.db.selectFrom(inner.as('feed_gens')).selectAll() + let builder = db.db.selectFrom(inner.as('feed_gens')).selectAll() const keyset = new LikeCountKeyset(ref('likeCount'), ref('cid')) builder = paginate(builder, { limit, cursor, keyset, direction: 'desc' }) diff --git a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts index 448af17e5f2..fa0e8626ca9 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts @@ -10,7 +10,9 @@ export default function (server: Server, ctx: AppContext) { const { limit, cursor } = params const viewer = auth.credentials.did - const skeleton = await getTimelineSkeleton(ctx.db, viewer, limit, cursor) + const db = ctx.db.getReplica('timeline') + const skeleton = await getTimelineSkeleton(db, viewer, limit, cursor) + return { encoding: 'application/json', body: skeleton, diff --git a/packages/bsky/src/api/blob-resolver.ts b/packages/bsky/src/api/blob-resolver.ts index 008bf4bb6a0..622590e10b4 100644 --- a/packages/bsky/src/api/blob-resolver.ts +++ b/packages/bsky/src/api/blob-resolver.ts @@ -10,7 +10,7 @@ import { TAKEDOWN } from '../lexicon/types/com/atproto/admin/defs' import AppContext from '../context' import { httpLogger as log } from '../logger' import { retryHttp } from '../util/retry' -import Database from '../db' +import { Database } from '../db' // Resolve and verify blob from its origin host @@ -32,7 +32,8 @@ export const createRouter = (ctx: AppContext): express.Router => { return next(createError(400, 'Invalid cid')) } - const verifiedImage = await resolveBlob(did, cid, ctx) + const db = ctx.db.getReplica() + const verifiedImage = await resolveBlob(did, cid, db, ctx.idResolver) // Send chunked response, destroying stream early (before // closing chunk) if the bytes don't match the expected cid. @@ -79,15 +80,13 @@ export const createRouter = (ctx: AppContext): express.Router => { export async function resolveBlob( did: string, cid: CID, - ctx: { - db: Database - idResolver: IdResolver - }, + db: Database, + idResolver: IdResolver, ) { const cidStr = cid.toString() const [{ pds }, takedown] = await Promise.all([ - ctx.idResolver.did.resolveAtprotoData(did), // @TODO cache did info - ctx.db.db + idResolver.did.resolveAtprotoData(did), // @TODO cache did info + db.db .selectFrom('moderation_action_subject_blob') .select('actionId') .innerJoin( diff --git a/packages/bsky/src/api/com/atproto/admin/getModerationAction.ts b/packages/bsky/src/api/com/atproto/admin/getModerationAction.ts index ee0bf59538f..55ff9b9ccf8 100644 --- a/packages/bsky/src/api/com/atproto/admin/getModerationAction.ts +++ b/packages/bsky/src/api/com/atproto/admin/getModerationAction.ts @@ -5,9 +5,9 @@ export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationAction({ auth: ctx.roleVerifier, handler: async ({ params }) => { - const { db, services } = ctx const { id } = params - const moderationService = services.moderation(db) + const db = ctx.db.getPrimary() + const moderationService = ctx.services.moderation(db) const result = await moderationService.getActionOrThrow(id) return { encoding: 'application/json', diff --git a/packages/bsky/src/api/com/atproto/admin/getModerationActions.ts b/packages/bsky/src/api/com/atproto/admin/getModerationActions.ts index 2231eb686c3..ef28ef10b7a 100644 --- a/packages/bsky/src/api/com/atproto/admin/getModerationActions.ts +++ b/packages/bsky/src/api/com/atproto/admin/getModerationActions.ts @@ -5,9 +5,9 @@ export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationActions({ auth: ctx.roleVerifier, handler: async ({ params }) => { - const { db, services } = ctx const { subject, limit = 50, cursor } = params - const moderationService = services.moderation(db) + const db = ctx.db.getPrimary() + const moderationService = ctx.services.moderation(db) const results = await moderationService.getActions({ subject, limit, diff --git a/packages/bsky/src/api/com/atproto/admin/getModerationReport.ts b/packages/bsky/src/api/com/atproto/admin/getModerationReport.ts index 1752a1481c1..e3faaa04436 100644 --- a/packages/bsky/src/api/com/atproto/admin/getModerationReport.ts +++ b/packages/bsky/src/api/com/atproto/admin/getModerationReport.ts @@ -5,9 +5,9 @@ export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationReport({ auth: ctx.roleVerifier, handler: async ({ params }) => { - const { db, services } = ctx const { id } = params - const moderationService = services.moderation(db) + const db = ctx.db.getPrimary() + const moderationService = ctx.services.moderation(db) const result = await moderationService.getReportOrThrow(id) return { encoding: 'application/json', diff --git a/packages/bsky/src/api/com/atproto/admin/getModerationReports.ts b/packages/bsky/src/api/com/atproto/admin/getModerationReports.ts index 85c53f78758..d3956973f37 100644 --- a/packages/bsky/src/api/com/atproto/admin/getModerationReports.ts +++ b/packages/bsky/src/api/com/atproto/admin/getModerationReports.ts @@ -5,7 +5,6 @@ export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getModerationReports({ auth: ctx.roleVerifier, handler: async ({ params }) => { - const { db, services } = ctx const { subject, resolved, @@ -17,7 +16,8 @@ export default function (server: Server, ctx: AppContext) { reporters = [], actionedBy, } = params - const moderationService = services.moderation(db) + const db = ctx.db.getPrimary() + const moderationService = ctx.services.moderation(db) const results = await moderationService.getReports({ subject, resolved, diff --git a/packages/bsky/src/api/com/atproto/admin/getRecord.ts b/packages/bsky/src/api/com/atproto/admin/getRecord.ts index 1847ee8e7b5..80e79fd94a2 100644 --- a/packages/bsky/src/api/com/atproto/admin/getRecord.ts +++ b/packages/bsky/src/api/com/atproto/admin/getRecord.ts @@ -6,8 +6,8 @@ export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getRecord({ auth: ctx.roleVerifier, handler: async ({ params }) => { - const { db, services } = ctx const { uri, cid } = params + const db = ctx.db.getPrimary() const result = await db.db .selectFrom('record') .selectAll() @@ -19,7 +19,7 @@ export default function (server: Server, ctx: AppContext) { } return { encoding: 'application/json', - body: await services.moderation(db).views.recordDetail(result), + body: await ctx.services.moderation(db).views.recordDetail(result), } }, }) diff --git a/packages/bsky/src/api/com/atproto/admin/getRepo.ts b/packages/bsky/src/api/com/atproto/admin/getRepo.ts index 1e00c565ded..5febdfcdd0c 100644 --- a/packages/bsky/src/api/com/atproto/admin/getRepo.ts +++ b/packages/bsky/src/api/com/atproto/admin/getRepo.ts @@ -6,15 +6,15 @@ export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.getRepo({ auth: ctx.roleVerifier, handler: async ({ params }) => { - const { db, services } = ctx const { did } = params - const result = await services.actor(db).getActor(did, true) + const db = ctx.db.getPrimary() + const result = await ctx.services.actor(db).getActor(did, true) if (!result) { throw new InvalidRequestError('Repo not found', 'RepoNotFound') } return { encoding: 'application/json', - body: await services.moderation(db).views.repoDetail(result), + body: await ctx.services.moderation(db).views.repoDetail(result), } }, }) diff --git a/packages/bsky/src/api/com/atproto/admin/resolveModerationReports.ts b/packages/bsky/src/api/com/atproto/admin/resolveModerationReports.ts index 6ec217b0655..ed420e7d820 100644 --- a/packages/bsky/src/api/com/atproto/admin/resolveModerationReports.ts +++ b/packages/bsky/src/api/com/atproto/admin/resolveModerationReports.ts @@ -5,12 +5,12 @@ export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.resolveModerationReports({ auth: ctx.roleVerifier, handler: async ({ input }) => { - const { db, services } = ctx - const moderationService = services.moderation(db) + const db = ctx.db.getPrimary() + const moderationService = ctx.services.moderation(db) const { actionId, reportIds, createdBy } = input.body const moderationAction = await db.transaction(async (dbTxn) => { - const moderationTxn = services.moderation(dbTxn) + const moderationTxn = ctx.services.moderation(dbTxn) await moderationTxn.resolveReports({ reportIds, actionId, createdBy }) return await moderationTxn.getActionOrThrow(actionId) }) diff --git a/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts b/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts index 2fe61649d27..e62f5371ad9 100644 --- a/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts +++ b/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts @@ -13,13 +13,13 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.roleVerifier, handler: async ({ input, auth }) => { const access = auth.credentials - const { db, services } = ctx - const moderationService = services.moderation(db) + const db = ctx.db.getPrimary() + const moderationService = ctx.services.moderation(db) const { id, createdBy, reason } = input.body const moderationAction = await db.transaction(async (dbTxn) => { - const moderationTxn = services.moderation(dbTxn) - const labelTxn = services.label(dbTxn) + const moderationTxn = ctx.services.moderation(dbTxn) + const labelTxn = ctx.services.label(dbTxn) const now = new Date() const existing = await moderationTxn.getAction(id) diff --git a/packages/bsky/src/api/com/atproto/admin/searchRepos.ts b/packages/bsky/src/api/com/atproto/admin/searchRepos.ts index 95e47d4078a..73b7c31e2b6 100644 --- a/packages/bsky/src/api/com/atproto/admin/searchRepos.ts +++ b/packages/bsky/src/api/com/atproto/admin/searchRepos.ts @@ -8,8 +8,8 @@ export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.searchRepos({ auth: ctx.roleVerifier, handler: async ({ params }) => { - const { db, services } = ctx - const moderationService = services.moderation(db) + const db = ctx.db.getPrimary() + const moderationService = ctx.services.moderation(db) const { term = '', limit, cursor, invitedBy } = params if (invitedBy) { throw new InvalidRequestError('The invitedBy parameter is unsupported') @@ -19,7 +19,10 @@ export default function (server: Server, ctx: AppContext) { const { ref } = db.db.dynamic const keyset = new ListKeyset(ref('indexedAt'), ref('did')) - let resultQb = services.actor(db).searchQb(searchField, term).selectAll() + let resultQb = ctx.services + .actor(db) + .searchQb(searchField, term) + .selectAll() resultQb = paginate(resultQb, { keyset, cursor, limit }) const results = await resultQb.execute() diff --git a/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts b/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts index d045475dcbb..195c0a63a73 100644 --- a/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts +++ b/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts @@ -15,8 +15,8 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.roleVerifier, handler: async ({ input, auth }) => { const access = auth.credentials - const { db, services } = ctx - const moderationService = services.moderation(db) + const db = ctx.db.getPrimary() + const moderationService = ctx.services.moderation(db) const { action, subject, @@ -52,8 +52,8 @@ export default function (server: Server, ctx: AppContext) { validateLabels([...(createLabelVals ?? []), ...(negateLabelVals ?? [])]) const moderationAction = await db.transaction(async (dbTxn) => { - const moderationTxn = services.moderation(dbTxn) - const labelTxn = services.label(dbTxn) + const moderationTxn = ctx.services.moderation(dbTxn) + const labelTxn = ctx.services.label(dbTxn) const result = await moderationTxn.logAction({ action: getAction(action), diff --git a/packages/bsky/src/api/com/atproto/identity/resolveHandle.ts b/packages/bsky/src/api/com/atproto/identity/resolveHandle.ts index c1e8f88a4c4..20865a058df 100644 --- a/packages/bsky/src/api/com/atproto/identity/resolveHandle.ts +++ b/packages/bsky/src/api/com/atproto/identity/resolveHandle.ts @@ -7,8 +7,9 @@ export default function (server: Server, ctx: AppContext) { server.com.atproto.identity.resolveHandle(async ({ req, params }) => { const handle = ident.normalizeHandle(params.handle || req.hostname) + const db = ctx.db.getReplica() let did: string | undefined - const user = await ctx.services.actor(ctx.db).getActor(handle, true) + const user = await ctx.services.actor(db).getActor(handle, true) if (user) { did = user.did } else { diff --git a/packages/bsky/src/api/com/atproto/moderation/createReport.ts b/packages/bsky/src/api/com/atproto/moderation/createReport.ts index 35bcde5a5e9..4cef67f1c65 100644 --- a/packages/bsky/src/api/com/atproto/moderation/createReport.ts +++ b/packages/bsky/src/api/com/atproto/moderation/createReport.ts @@ -9,19 +9,20 @@ export default function (server: Server, ctx: AppContext) { // @TODO anonymous reports w/ optional auth are a temporary measure auth: ctx.authOptionalVerifier, handler: async ({ input, auth }) => { - const { db, services } = ctx 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 services.actor(db).getActor(requester, true) + const actor = await ctx.services.actor(db).getActor(requester, true) if (actor && softDeleted(actor)) { throw new AuthRequiredError() } } - const moderationService = services.moderation(db) + const moderationService = ctx.services.moderation(db) const report = await moderationService.report({ reasonType: getReasonType(reasonType), diff --git a/packages/bsky/src/api/com/atproto/repo/getRecord.ts b/packages/bsky/src/api/com/atproto/repo/getRecord.ts index 9903d171b7f..f96da38cdf6 100644 --- a/packages/bsky/src/api/com/atproto/repo/getRecord.ts +++ b/packages/bsky/src/api/com/atproto/repo/getRecord.ts @@ -7,14 +7,15 @@ import { jsonStringToLex } from '@atproto/lexicon' export default function (server: Server, ctx: AppContext) { server.com.atproto.repo.getRecord(async ({ params }) => { const { repo, collection, rkey, cid } = params - const did = await ctx.services.actor(ctx.db).getActorDid(repo) + const db = ctx.db.getReplica() + const did = await ctx.services.actor(db).getActorDid(repo) if (!did) { throw new InvalidRequestError(`Could not find repo: ${repo}`) } const uri = AtUri.make(did, collection, rkey) - let builder = ctx.db.db + let builder = db.db .selectFrom('record') .selectAll() .where('uri', '=', uri.toString()) diff --git a/packages/bsky/src/api/health.ts b/packages/bsky/src/api/health.ts index ef14882625d..bdcdeefcb4b 100644 --- a/packages/bsky/src/api/health.ts +++ b/packages/bsky/src/api/health.ts @@ -7,8 +7,9 @@ export const createRouter = (ctx: AppContext): express.Router => { router.get('/xrpc/_health', async function (req, res) { const { version } = ctx.cfg + const db = ctx.db.getPrimary() try { - await sql`select 1`.execute(ctx.db.db) + await sql`select 1`.execute(db.db) } catch (err) { req.log.error(err, 'failed health check') return res.status(503).send({ version, error: 'Service Unavailable' }) diff --git a/packages/bsky/src/background.ts b/packages/bsky/src/background.ts index dbf47eb8868..466bad80a51 100644 --- a/packages/bsky/src/background.ts +++ b/packages/bsky/src/background.ts @@ -1,5 +1,5 @@ import PQueue from 'p-queue' -import Database from './db' +import { PrimaryDatabase } from './db' import { dbLogger } from './logger' // A simple queue for in-process, out-of-band/backgrounded work @@ -7,7 +7,7 @@ import { dbLogger } from './logger' export class BackgroundQueue { queue = new PQueue({ concurrency: 20 }) destroyed = false - constructor(public db: Database) {} + constructor(public db: PrimaryDatabase) {} add(task: Task) { if (this.destroyed) { @@ -32,4 +32,4 @@ export class BackgroundQueue { } } -type Task = (db: Database) => Promise +type Task = (db: PrimaryDatabase) => Promise diff --git a/packages/bsky/src/bin.ts b/packages/bsky/src/bin.ts deleted file mode 100644 index 97494c59d07..00000000000 --- a/packages/bsky/src/bin.ts +++ /dev/null @@ -1,25 +0,0 @@ -import './env' -import { ServerConfig } from './config' -import Database from './db' -import BskyAppView from './index' -import { AddressInfo } from 'net' - -const run = async () => { - const cfg = ServerConfig.readEnv() - const db = Database.postgres({ - url: cfg.dbPostgresUrl, - schema: cfg.dbPostgresSchema, - }) - - await db.migrateToLatestOrThrow() - - const bsky = BskyAppView.create({ db, config: cfg }) - await bsky.start() - - const { address, port, family } = bsky.server?.address() as AddressInfo - const location = - family === 'IPv6' ? `[${address}]:${port}` : `${address}:${port}` - console.log(`🌞 Bsky App View is running at ${location}`) -} - -run() diff --git a/packages/bsky/src/config.ts b/packages/bsky/src/config.ts index f5f48aa3445..e1ecf89ded0 100644 --- a/packages/bsky/src/config.ts +++ b/packages/bsky/src/config.ts @@ -8,7 +8,9 @@ export interface ServerConfigValues { publicUrl?: string serverDid: string feedGenDid?: string - dbPostgresUrl: string + dbPrimaryPostgresUrl: string + dbReplicaPostgresUrls?: string[] + dbReplicaTags?: Record // E.g. { timeline: [0], thread: [1] } dbPostgresSchema?: string didPlcUrl: string didCacheStaleTTL: number @@ -44,10 +46,27 @@ export class ServerConfig { ) const imgUriEndpoint = process.env.IMG_URI_ENDPOINT const blobCacheLocation = process.env.BLOB_CACHE_LOC - const dbPostgresUrl = - overrides?.dbPostgresUrl || process.env.DB_POSTGRES_URL - assert(dbPostgresUrl) + const dbPrimaryPostgresUrl = + overrides?.dbPrimaryPostgresUrl || process.env.DB_PRIMARY_POSTGRES_URL + let dbReplicaPostgresUrls = overrides?.dbReplicaPostgresUrls + if (!dbReplicaPostgresUrls && process.env.DB_REPLICA_POSTGRES_URLS) { + dbReplicaPostgresUrls = process.env.DB_REPLICA_POSTGRES_URLS.split(',') + } + const dbReplicaTags = overrides?.dbReplicaTags ?? { + '*': getTagIdxs(process.env.DB_REPLICA_TAGS_ANY), // e.g. DB_REPLICA_TAGS_ANY=0,1 + timeline: getTagIdxs(process.env.DB_REPLICA_TAGS_TIMELINE), + feed: getTagIdxs(process.env.DB_REPLICA_TAGS_FEED), + search: getTagIdxs(process.env.DB_REPLICA_TAGS_SEARCH), + thread: getTagIdxs(process.env.DB_REPLICA_TAGS_THREAD), + } + assert( + Object.values(dbReplicaTags) + .flat() + .every((idx) => idx < (dbReplicaPostgresUrls?.length ?? 0)), + 'out of range index in replica tags', + ) const dbPostgresSchema = process.env.DB_POSTGRES_SCHEMA + assert(dbPrimaryPostgresUrl) const adminPassword = process.env.ADMIN_PASSWORD || 'admin' const moderatorPassword = process.env.MODERATOR_PASSWORD || undefined const triagePassword = process.env.TRIAGE_PASSWORD || undefined @@ -59,7 +78,9 @@ export class ServerConfig { publicUrl, serverDid, feedGenDid, - dbPostgresUrl, + dbPrimaryPostgresUrl, + dbReplicaPostgresUrls, + dbReplicaTags, dbPostgresSchema, didPlcUrl, didCacheStaleTTL, @@ -111,8 +132,16 @@ export class ServerConfig { return this.cfg.feedGenDid } - get dbPostgresUrl() { - return this.cfg.dbPostgresUrl + get dbPrimaryPostgresUrl() { + return this.cfg.dbPrimaryPostgresUrl + } + + get dbReplicaPostgresUrl() { + return this.cfg.dbReplicaPostgresUrls + } + + get dbReplicaTags() { + return this.cfg.dbReplicaTags } get dbPostgresSchema() { @@ -156,6 +185,10 @@ export class ServerConfig { } } +function getTagIdxs(str?: string): number[] { + return str ? str.split(',').map((item) => parseInt(item, 10)) : [] +} + function stripUndefineds( obj: Record, ): Record { diff --git a/packages/bsky/src/context.ts b/packages/bsky/src/context.ts index ce02fd41ffc..ed322e411a1 100644 --- a/packages/bsky/src/context.ts +++ b/packages/bsky/src/context.ts @@ -1,6 +1,6 @@ import * as plc from '@did-plc/lib' import { IdResolver } from '@atproto/identity' -import { Database } from './db' +import { DatabaseCoordinator } from './db' import { ServerConfig } from './config' import { ImageUriBuilder } from './image/uri' import { Services } from './services' @@ -13,7 +13,7 @@ import { LabelCache } from './label-cache' export class AppContext { constructor( private opts: { - db: Database + db: DatabaseCoordinator imgUriBuilder: ImageUriBuilder cfg: ServerConfig services: Services @@ -25,7 +25,7 @@ export class AppContext { }, ) {} - get db(): Database { + get db(): DatabaseCoordinator { return this.opts.db } diff --git a/packages/bsky/src/db/coordinator.ts b/packages/bsky/src/db/coordinator.ts new file mode 100644 index 00000000000..64933b8e2fe --- /dev/null +++ b/packages/bsky/src/db/coordinator.ts @@ -0,0 +1,107 @@ +import { Migrator } from 'kysely' +import PrimaryDatabase from './primary' +import Database from './db' +import { PgOptions } from './types' +import { dbLogger } from '../logger' + +type ReplicaTag = 'timeline' | 'feed' | 'search' | 'thread' | '*' +type ReplicaOptions = PgOptions & { tags?: ReplicaTag[] } + +type CoordinatorOptions = { + schema?: string + primary: PgOptions + replicas?: ReplicaOptions[] +} + +type ReplicaGroup = { + dbs: Database[] + roundRobinIdx: number +} + +export class DatabaseCoordinator { + migrator: Migrator + destroyed = false + + private primary: PrimaryDatabase + private allReplicas: Database[] + private tagged: Record + private untagged: ReplicaGroup + private tagWarns = new Set() + + constructor(public opts: CoordinatorOptions) { + this.primary = new PrimaryDatabase({ + schema: opts.schema, + ...opts.primary, + }) + this.allReplicas = [] + this.tagged = {} + this.untagged = { + dbs: [], + roundRobinIdx: 0, + } + for (const cfg of opts.replicas ?? []) { + const db = new Database({ + schema: opts.schema, + ...cfg, + }) + this.allReplicas.push(db) + // setup different groups of replicas based on tag, each round-robins separately. + if (cfg.tags) { + for (const tag of cfg.tags) { + if (tag === '*') { + this.untagged.dbs.push(db) + } else { + this.tagged[tag] ??= { + dbs: [], + roundRobinIdx: 0, + } + this.tagged[tag].dbs.push(db) + } + } + } else { + this.untagged.dbs.push(db) + } + } + // guarantee there is always a replica around to service any query, falling back to primary. + if (!this.untagged.dbs.length) { + if (this.allReplicas.length) { + this.untagged.dbs = [...this.allReplicas] + } else { + this.untagged.dbs = [this.primary] + } + } + } + + getPrimary(): PrimaryDatabase { + return this.primary + } + + getReplicas(): Database[] { + return this.allReplicas + } + + getReplica(tag?: ReplicaTag): Database { + if (tag && this.tagged[tag]) { + return nextDb(this.tagged[tag]) + } + if (tag && !this.tagWarns.has(tag)) { + this.tagWarns.add(tag) + dbLogger.warn({ tag }, 'no replica for tag, falling back to any replica') + } + return nextDb(this.untagged) + } + + async close(): Promise { + await Promise.all([ + this.primary.close(), + ...this.allReplicas.map((db) => db.close()), + ]) + } +} + +// @NOTE mutates group incrementing roundRobinIdx +const nextDb = (group: ReplicaGroup) => { + const db = group.dbs[group.roundRobinIdx] + group.roundRobinIdx = (group.roundRobinIdx + 1) % group.dbs.length + return db +} diff --git a/packages/bsky/src/db/db.ts b/packages/bsky/src/db/db.ts new file mode 100644 index 00000000000..7c984892ea1 --- /dev/null +++ b/packages/bsky/src/db/db.ts @@ -0,0 +1,85 @@ +import assert from 'assert' +import { Kysely, PostgresDialect } from 'kysely' +import { Pool as PgPool, types as pgTypes } from 'pg' +import DatabaseSchema, { DatabaseSchemaType } from './database-schema' +import { PgOptions } from './types' + +export class Database { + pool: PgPool + db: DatabaseSchema + 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('connect', (client) => { + // 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 }), + }) + } + + 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') + } + + asPrimary(): Database { + throw new Error('Primary db required') + } + + async close(): Promise { + if (this.destroyed) return + await this.db.destroy() + this.destroyed = true + } +} + +export default Database diff --git a/packages/bsky/src/db/index.ts b/packages/bsky/src/db/index.ts index 8ca09b0f20d..1c5886fb10e 100644 --- a/packages/bsky/src/db/index.ts +++ b/packages/bsky/src/db/index.ts @@ -1,233 +1,3 @@ -import assert from 'assert' -import EventEmitter from 'events' -import { - Kysely, - PostgresDialect, - Migrator, - KyselyPlugin, - PluginTransformQueryArgs, - PluginTransformResultArgs, - RootOperationNode, - QueryResult, - UnknownRow, - sql, -} from 'kysely' -import { Pool as PgPool, types as pgTypes } from 'pg' -import TypedEmitter from 'typed-emitter' -import { wait } from '@atproto/common' -import DatabaseSchema, { DatabaseSchemaType } from './database-schema' -import * as migrations from './migrations' -import { CtxMigrationProvider } from './migrations/provider' -import { dbLogger as log } from '../logger' - -export class Database { - migrator: Migrator - txEvt = new EventEmitter() as TxnEmitter - destroyed = false - - constructor(public db: DatabaseSchema, public cfg: PgConfig) { - this.migrator = new Migrator({ - db, - migrationTableSchema: cfg.schema, - provider: new CtxMigrationProvider(migrations, cfg.dialect), - }) - } - - static postgres(opts: PgOptions): Database { - 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('connect', (client) => { - // 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;`) - } - }) - - const db = new Kysely({ - dialect: new PostgresDialect({ pool }), - }) - - return new Database(db, { dialect: 'pg', pool, schema, url }) - } - - 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(txn, this.cfg) - 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) - } - - get schema(): string | undefined { - return this.cfg.dialect === 'pg' ? this.cfg.schema : undefined - } - - get isTransaction() { - return this.db.isTransaction - } - - assertTransaction() { - assert(this.isTransaction, 'Transaction required') - } - - assertNotTransaction() { - assert(!this.isTransaction, 'Cannot be in a transaction') - } - - 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 - } - - async maintainMaterializedViews(opts: { - views: string[] - intervalSec: number - signal: AbortSignal - }) { - const { views, intervalSec, signal } = opts - while (!signal.aborted) { - // super basic synchronization by agreeing when the intervals land relative to unix timestamp - const now = Date.now() - const intervalMs = 1000 * intervalSec - const nextIteration = Math.ceil(now / intervalMs) - const nextInMs = nextIteration * intervalMs - now - await wait(nextInMs) - if (signal.aborted) break - await Promise.all( - views.map(async (view) => { - try { - await this.refreshMaterializedView(view) - log.info( - { view, time: new Date().toISOString() }, - 'materialized view refreshed', - ) - } catch (err) { - log.error( - { view, err, time: new Date().toISOString() }, - 'materialized view refresh failed', - ) - } - }), - ) - } - } - - async refreshMaterializedView(view: string) { - const { ref } = this.db.dynamic - await sql`refresh materialized view concurrently ${ref(view)}`.execute( - this.db, - ) - } -} - -export default Database - -export type PgConfig = { - dialect: 'pg' - pool: PgPool - url: string - schema?: string -} - -type PgOptions = { - url: string - pool?: PgPool - schema?: string - poolSize?: number - poolMaxUses?: number - poolIdleTimeoutMs?: number -} - -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 () => {} +export * from './primary' +export * from './db' +export * from './coordinator' diff --git a/packages/bsky/src/db/leader.ts b/packages/bsky/src/db/leader.ts index 20b6757a919..ebd44bf98d6 100644 --- a/packages/bsky/src/db/leader.ts +++ b/packages/bsky/src/db/leader.ts @@ -1,9 +1,9 @@ import { PoolClient } from 'pg' -import Database from '.' +import PrimaryDatabase from './primary' export class Leader { session: Session | null = null - constructor(public id: number, public db: Database) {} + constructor(public id: number, public db: PrimaryDatabase) {} async run( task: (ctx: { signal: AbortSignal }) => Promise, @@ -29,7 +29,7 @@ export class Leader { // Postgres implementation uses advisory locking, automatically released by ending connection. - const client = await this.db.cfg.pool.connect() + const client = await this.db.pool.connect() try { const lock = await client.query( 'SELECT pg_try_advisory_lock($1) as acquired', diff --git a/packages/bsky/src/db/primary.ts b/packages/bsky/src/db/primary.ts new file mode 100644 index 00000000000..e6e69872fd5 --- /dev/null +++ b/packages/bsky/src/db/primary.ts @@ -0,0 +1,184 @@ +import EventEmitter from 'events' +import { + Migrator, + KyselyPlugin, + PluginTransformQueryArgs, + PluginTransformResultArgs, + RootOperationNode, + QueryResult, + UnknownRow, + sql, +} from 'kysely' +import { Pool as PgPool } from 'pg' +import TypedEmitter from 'typed-emitter' +import { wait } from '@atproto/common' +import DatabaseSchema from './database-schema' +import * as migrations from './migrations' +import { CtxMigrationProvider } from './migrations/provider' +import { dbLogger as log } from '../logger' +import { PgOptions } from './types' +import { Database } from './db' + +export class PrimaryDatabase extends Database { + migrator: Migrator + txEvt = new EventEmitter() as TxnEmitter + destroyed = false + isPrimary = true + + constructor( + public opts: PgOptions, + instances?: { db: DatabaseSchema; pool: PgPool }, + ) { + super(opts, instances) + this.migrator = new Migrator({ + db: this.db, + migrationTableSchema: opts.schema, + provider: new CtxMigrationProvider(migrations, 'pg'), + }) + } + + static is(db: Database): db is PrimaryDatabase { + return db.isPrimary + } + + asPrimary(): PrimaryDatabase { + return this + } + + async transaction(fn: (db: PrimaryDatabase) => Promise): Promise { + const leakyTxPlugin = new LeakyTxPlugin() + const { dbTxn, txRes } = await this.db + .withPlugin(leakyTxPlugin) + .transaction() + .execute(async (txn) => { + const dbTxn = new PrimaryDatabase(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 + } + + async maintainMaterializedViews(opts: { + views: string[] + intervalSec: number + signal: AbortSignal + }) { + const { views, intervalSec, signal } = opts + while (!signal.aborted) { + // super basic synchronization by agreeing when the intervals land relative to unix timestamp + const now = Date.now() + const intervalMs = 1000 * intervalSec + const nextIteration = Math.ceil(now / intervalMs) + const nextInMs = nextIteration * intervalMs - now + await wait(nextInMs) + if (signal.aborted) break + await Promise.all( + views.map(async (view) => { + try { + await this.refreshMaterializedView(view) + log.info( + { view, time: new Date().toISOString() }, + 'materialized view refreshed', + ) + } catch (err) { + log.error( + { view, err, time: new Date().toISOString() }, + 'materialized view refresh failed', + ) + } + }), + ) + } + } + + async refreshMaterializedView(view: string) { + const { ref } = this.db.dynamic + await sql`refresh materialized view concurrently ${ref(view)}`.execute( + this.db, + ) + } +} + +export default PrimaryDatabase + +// 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/bsky/src/db/types.ts b/packages/bsky/src/db/types.ts new file mode 100644 index 00000000000..6dd5f084c80 --- /dev/null +++ b/packages/bsky/src/db/types.ts @@ -0,0 +1,10 @@ +import { Pool as PgPool } from 'pg' + +export type PgOptions = { + url: string + pool?: PgPool + schema?: string + poolSize?: number + poolMaxUses?: number + poolIdleTimeoutMs?: number +} diff --git a/packages/bsky/src/db/views.ts b/packages/bsky/src/db/views.ts index 0608cc0dad0..d5aa9941436 100644 --- a/packages/bsky/src/db/views.ts +++ b/packages/bsky/src/db/views.ts @@ -1,7 +1,7 @@ import { jitter, wait } from '@atproto/common' import { Leader } from './leader' import { dbLogger } from '../logger' -import Database from '.' +import { PrimaryDatabase } from '.' export const VIEW_MAINTAINER_ID = 1010 const VIEWS = ['algo_whats_hot_view'] @@ -11,7 +11,7 @@ export class ViewMaintainer { destroyed = false // @NOTE the db must be authed as the owner of the materialized view, per postgres. - constructor(public db: Database, public intervalSec = 60) {} + constructor(public db: PrimaryDatabase, public intervalSec = 60) {} async run() { while (!this.destroyed) { diff --git a/packages/bsky/src/did-cache.ts b/packages/bsky/src/did-cache.ts index da8854871cf..5da647b0108 100644 --- a/packages/bsky/src/did-cache.ts +++ b/packages/bsky/src/did-cache.ts @@ -1,13 +1,15 @@ import PQueue from 'p-queue' import { CacheResult, DidCache, DidDocument } from '@atproto/identity' -import Database from './db' +import { PrimaryDatabase } from './db' import { dbLogger } from './logger' export class DidSqlCache implements DidCache { public pQueue: PQueue | null //null during teardown constructor( - public db: Database, + // @TODO perhaps could use both primary and non-primary. not high enough + // throughput to matter right now. also may just move this over to redis before long! + public db: PrimaryDatabase, public staleTTL: number, public maxTTL: number, ) { diff --git a/packages/bsky/src/feed-gen/best-of-follows.ts b/packages/bsky/src/feed-gen/best-of-follows.ts index ba263b3ee6f..c154ad9aa0e 100644 --- a/packages/bsky/src/feed-gen/best-of-follows.ts +++ b/packages/bsky/src/feed-gen/best-of-follows.ts @@ -10,10 +10,11 @@ const handler: AlgoHandler = async ( viewer: string, ): Promise => { const { limit, cursor } = params - const feedService = ctx.services.feed(ctx.db) - const graphService = ctx.services.graph(ctx.db) + const db = ctx.db.getReplica('feed') + const feedService = ctx.services.feed(db) + const graphService = ctx.services.graph(db) - const { ref } = ctx.db.db.dynamic + const { ref } = db.db.dynamic // candidates are ranked within a materialized view by like count, depreciated over time. diff --git a/packages/bsky/src/feed-gen/bsky-team.ts b/packages/bsky/src/feed-gen/bsky-team.ts index 537a259cd31..40e9cc63fe5 100644 --- a/packages/bsky/src/feed-gen/bsky-team.ts +++ b/packages/bsky/src/feed-gen/bsky-team.ts @@ -17,10 +17,11 @@ const handler: AlgoHandler = async ( viewer: string, ): Promise => { const { limit = 50, cursor } = params - const feedService = ctx.services.feed(ctx.db) - const graphService = ctx.services.graph(ctx.db) + const db = ctx.db.getReplica('feed') + const feedService = ctx.services.feed(db) + const graphService = ctx.services.graph(db) - const { ref } = ctx.db.db.dynamic + const { ref } = db.db.dynamic const postsQb = feedService .selectPostQb() @@ -32,7 +33,7 @@ const handler: AlgoHandler = async ( const keyset = new FeedKeyset(ref('sortAt'), ref('cid')) - let feedQb = ctx.db.db.selectFrom(postsQb.as('feed_items')).selectAll() + let feedQb = db.db.selectFrom(postsQb.as('feed_items')).selectAll() feedQb = paginate(feedQb, { limit, cursor, keyset }) const feedItems = await feedQb.execute() diff --git a/packages/bsky/src/feed-gen/hot-classic.ts b/packages/bsky/src/feed-gen/hot-classic.ts index 67a9522d0c3..fb191328002 100644 --- a/packages/bsky/src/feed-gen/hot-classic.ts +++ b/packages/bsky/src/feed-gen/hot-classic.ts @@ -14,10 +14,11 @@ const handler: AlgoHandler = async ( viewer: string, ): Promise => { const { limit = 50, cursor } = params - const feedService = ctx.services.feed(ctx.db) - const graphService = ctx.services.graph(ctx.db) + const db = ctx.db.getReplica('feed') + const feedService = ctx.services.feed(db) + const graphService = ctx.services.graph(db) - const { ref } = ctx.db.db.dynamic + const { ref } = db.db.dynamic const postsQb = feedService .selectPostQb() @@ -45,7 +46,7 @@ const handler: AlgoHandler = async ( const keyset = new FeedKeyset(ref('sortAt'), ref('cid')) - let feedQb = ctx.db.db.selectFrom(postsQb.as('feed_items')).selectAll() + let feedQb = db.db.selectFrom(postsQb.as('feed_items')).selectAll() feedQb = paginate(feedQb, { limit, cursor, keyset }) const feedItems = await feedQb.execute() diff --git a/packages/bsky/src/feed-gen/mutuals.ts b/packages/bsky/src/feed-gen/mutuals.ts index d81ee46dd2f..c12ef713ada 100644 --- a/packages/bsky/src/feed-gen/mutuals.ts +++ b/packages/bsky/src/feed-gen/mutuals.ts @@ -10,12 +10,13 @@ const handler: AlgoHandler = async ( viewer: string, ): Promise => { const { limit = 50, cursor } = params - const feedService = ctx.services.feed(ctx.db) - const graphService = ctx.services.graph(ctx.db) + const db = ctx.db.getReplica('feed') + const feedService = ctx.services.feed(db) + const graphService = ctx.services.graph(db) - const { ref } = ctx.db.db.dynamic + const { ref } = db.db.dynamic - const mutualsSubquery = ctx.db.db + const mutualsSubquery = db.db .selectFrom('follow') .where('follow.creator', '=', viewer) .whereExists((qb) => diff --git a/packages/bsky/src/feed-gen/whats-hot.ts b/packages/bsky/src/feed-gen/whats-hot.ts index 126f4a66e4f..1d74f72fcab 100644 --- a/packages/bsky/src/feed-gen/whats-hot.ts +++ b/packages/bsky/src/feed-gen/whats-hot.ts @@ -24,13 +24,14 @@ const handler: AlgoHandler = async ( viewer: string, ): Promise => { const { limit, cursor } = params - const graphService = ctx.services.graph(ctx.db) + const db = ctx.db.getReplica('feed') + const graphService = ctx.services.graph(db) - const { ref } = ctx.db.db.dynamic + const { ref } = db.db.dynamic // candidates are ranked within a materialized view by like count, depreciated over time. - let builder = ctx.db.db + let builder = db.db .selectFrom('algo_whats_hot_view as candidate') .innerJoin('post', 'post.uri', 'candidate.uri') .leftJoin('post_embed_record', 'post_embed_record.postUri', 'candidate.uri') diff --git a/packages/bsky/src/feed-gen/with-friends.ts b/packages/bsky/src/feed-gen/with-friends.ts index 0c53f9f4bc4..efd4b961e9d 100644 --- a/packages/bsky/src/feed-gen/with-friends.ts +++ b/packages/bsky/src/feed-gen/with-friends.ts @@ -10,9 +10,10 @@ const handler: AlgoHandler = async ( requester: string, ): Promise => { const { cursor, limit = 50 } = params - const feedService = ctx.services.feed(ctx.db) + const db = ctx.db.getReplica('feed') + const feedService = ctx.services.feed(db) - const { ref } = ctx.db.db.dynamic + const { ref } = db.db.dynamic const keyset = new FeedKeyset(ref('post.indexedAt'), ref('post.cid')) const sortFrom = keyset.unpack(cursor)?.primary diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index ae96ec18a1a..213c34374b7 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -7,7 +7,7 @@ import cors from 'cors' import compression from 'compression' import { IdResolver } from '@atproto/identity' import API, { health, blobResolver } from './api' -import Database from './db' +import { DatabaseCoordinator } from './db' import * as error from './error' import { dbLogger, loggerMiddleware } from './logger' import { ServerConfig } from './config' @@ -28,7 +28,7 @@ import { LabelCache } from './label-cache' export type { ServerConfigValues } from './config' export type { MountedAlgos } from './feed-gen/types' export { ServerConfig } from './config' -export { Database } from './db' +export { Database, PrimaryDatabase, DatabaseCoordinator } from './db' export { Redis } from './redis' export { ViewMaintainer } from './db/views' export { AppContext } from './context' @@ -49,7 +49,7 @@ export class BskyAppView { } static create(opts: { - db: Database + db: DatabaseCoordinator config: ServerConfig imgInvalidator?: ImageInvalidator algos?: MountedAlgos @@ -62,7 +62,7 @@ export class BskyAppView { app.use(compression()) const didCache = new DidSqlCache( - db, + db.getPrimary(), config.didCacheStaleTTL, config.didCacheMaxTTL, ) @@ -91,8 +91,8 @@ export class BskyAppView { throw new Error('Missing appview image invalidator') } - const backgroundQueue = new BackgroundQueue(db) - const labelCache = new LabelCache(db) + const backgroundQueue = new BackgroundQueue(db.getPrimary()) + const labelCache = new LabelCache(db.getPrimary()) const services = createServices({ imgUriBuilder, @@ -136,13 +136,26 @@ export class BskyAppView { async start(): Promise { const { db, backgroundQueue } = this.ctx - const { pool } = db.cfg + const primary = db.getPrimary() + const replicas = db.getReplicas() this.dbStatsInterval = setInterval(() => { dbLogger.info( { - idleCount: pool.idleCount, - totalCount: pool.totalCount, - waitingCount: pool.waitingCount, + idleCount: replicas.reduce( + (tot, replica) => tot + replica.pool.idleCount, + 0, + ), + totalCount: replicas.reduce( + (tot, replica) => tot + replica.pool.totalCount, + 0, + ), + waitingCount: replicas.reduce( + (tot, replica) => tot + replica.pool.waitingCount, + 0, + ), + primaryIdleCount: primary.pool.idleCount, + primaryTotalCount: primary.pool.totalCount, + primaryWaitingCount: primary.pool.waitingCount, }, 'db pool stats', ) diff --git a/packages/bsky/src/indexer/config.ts b/packages/bsky/src/indexer/config.ts index 619c245dd0e..24452c67174 100644 --- a/packages/bsky/src/indexer/config.ts +++ b/packages/bsky/src/indexer/config.ts @@ -29,7 +29,7 @@ export class IndexerConfig { static readEnv(overrides?: Partial) { const version = process.env.BSKY_VERSION || '0.0.0' const dbPostgresUrl = - overrides?.dbPostgresUrl || process.env.DB_POSTGRES_URL + overrides?.dbPostgresUrl || process.env.DB_PRIMARY_POSTGRES_URL const dbPostgresSchema = overrides?.dbPostgresSchema || process.env.DB_POSTGRES_SCHEMA const redisHost = diff --git a/packages/bsky/src/indexer/context.ts b/packages/bsky/src/indexer/context.ts index e8f9b6abae5..34cc43a227d 100644 --- a/packages/bsky/src/indexer/context.ts +++ b/packages/bsky/src/indexer/context.ts @@ -1,5 +1,5 @@ import { IdResolver } from '@atproto/identity' -import { Database } from '../db' +import { PrimaryDatabase } from '../db' import { IndexerConfig } from './config' import { Services } from './services' import { BackgroundQueue } from '../background' @@ -9,7 +9,7 @@ import { Redis } from '../redis' export class IndexerContext { constructor( private opts: { - db: Database + db: PrimaryDatabase redis: Redis cfg: IndexerConfig services: Services @@ -19,7 +19,7 @@ export class IndexerContext { }, ) {} - get db(): Database { + get db(): PrimaryDatabase { return this.opts.db } diff --git a/packages/bsky/src/indexer/index.ts b/packages/bsky/src/indexer/index.ts index ccb26c3311e..e4c4afed1ab 100644 --- a/packages/bsky/src/indexer/index.ts +++ b/packages/bsky/src/indexer/index.ts @@ -1,6 +1,6 @@ import { IdResolver } from '@atproto/identity' import { BackgroundQueue } from '../background' -import Database from '../db' +import { PrimaryDatabase } from '../db' import DidSqlCache from '../did-cache' import log from './logger' import { dbLogger } from '../logger' @@ -26,7 +26,7 @@ export class BskyIndexer { } static create(opts: { - db: Database + db: PrimaryDatabase redis: Redis cfg: IndexerConfig }): BskyIndexer { @@ -75,7 +75,7 @@ export class BskyIndexer { async start() { const { db, backgroundQueue } = this.ctx - const { pool } = db.cfg + const pool = db.pool this.dbStatsInterval = setInterval(() => { dbLogger.info( { diff --git a/packages/bsky/src/indexer/services.ts b/packages/bsky/src/indexer/services.ts index 6bb06c0ffca..362fed513d0 100644 --- a/packages/bsky/src/indexer/services.ts +++ b/packages/bsky/src/indexer/services.ts @@ -1,5 +1,5 @@ import { IdResolver } from '@atproto/identity' -import Database from '../db' +import { PrimaryDatabase } from '../db' import { Labeler } from '../labeler' import { BackgroundQueue } from '../background' import { IndexingService } from '../services/indexing' @@ -18,8 +18,8 @@ export function createServices(resources: { } export type Services = { - indexing: FromDb - label: FromDb + indexing: FromDbPrimary + label: FromDbPrimary } -type FromDb = (db: Database) => T +type FromDbPrimary = (db: PrimaryDatabase) => T diff --git a/packages/bsky/src/ingester/config.ts b/packages/bsky/src/ingester/config.ts index 49bcf700334..969aeeff7aa 100644 --- a/packages/bsky/src/ingester/config.ts +++ b/packages/bsky/src/ingester/config.ts @@ -23,7 +23,7 @@ export class IngesterConfig { static readEnv(overrides?: Partial) { const version = process.env.BSKY_VERSION || '0.0.0' const dbPostgresUrl = - overrides?.dbPostgresUrl || process.env.DB_POSTGRES_URL + overrides?.dbPostgresUrl || process.env.DB_PRIMARY_POSTGRES_URL const dbPostgresSchema = overrides?.dbPostgresSchema || process.env.DB_POSTGRES_SCHEMA const redisHost = diff --git a/packages/bsky/src/ingester/context.ts b/packages/bsky/src/ingester/context.ts index 3dca2bd9762..792d3c2015a 100644 --- a/packages/bsky/src/ingester/context.ts +++ b/packages/bsky/src/ingester/context.ts @@ -1,17 +1,17 @@ -import { Database } from '../db' +import { PrimaryDatabase } from '../db' import { Redis } from '../redis' import { IngesterConfig } from './config' export class IngesterContext { constructor( private opts: { - db: Database + db: PrimaryDatabase redis: Redis cfg: IngesterConfig }, ) {} - get db(): Database { + get db(): PrimaryDatabase { return this.opts.db } diff --git a/packages/bsky/src/ingester/index.ts b/packages/bsky/src/ingester/index.ts index 36096a8be9b..376da2887da 100644 --- a/packages/bsky/src/ingester/index.ts +++ b/packages/bsky/src/ingester/index.ts @@ -1,4 +1,4 @@ -import Database from '../db' +import { PrimaryDatabase } from '../db' import log from './logger' import { dbLogger } from '../logger' import { Redis } from '../redis' @@ -21,7 +21,7 @@ export class BskyIngester { } static create(opts: { - db: Database + db: PrimaryDatabase redis: Redis cfg: IngesterConfig }): BskyIngester { @@ -40,7 +40,7 @@ export class BskyIngester { async start() { const { db } = this.ctx - const { pool } = db.cfg + const pool = db.pool this.dbStatsInterval = setInterval(() => { dbLogger.info( { diff --git a/packages/bsky/src/label-cache.ts b/packages/bsky/src/label-cache.ts index 601152bee43..b162a2d30bd 100644 --- a/packages/bsky/src/label-cache.ts +++ b/packages/bsky/src/label-cache.ts @@ -1,5 +1,5 @@ import { wait } from '@atproto/common' -import Database from './db' +import { PrimaryDatabase } from './db' import { Label } from './db/tables/label' import { labelerLogger as log } from './logger' @@ -10,7 +10,7 @@ export class LabelCache { destroyed = false - constructor(public db: Database) {} + constructor(public db: PrimaryDatabase) {} start() { this.poll() diff --git a/packages/bsky/src/labeler/base.ts b/packages/bsky/src/labeler/base.ts index 71e935b6f87..7e792310f4a 100644 --- a/packages/bsky/src/labeler/base.ts +++ b/packages/bsky/src/labeler/base.ts @@ -3,7 +3,7 @@ import { AtpAgent } from '@atproto/api' import { cidForRecord } from '@atproto/repo' import { dedupe, getFieldsFromRecord } from './util' import { labelerLogger as log } from '../logger' -import Database from '../db' +import { PrimaryDatabase } from '../db' import { IdResolver } from '@atproto/identity' import { BackgroundQueue } from '../background' import { IndexerConfig } from '../indexer/config' @@ -15,7 +15,7 @@ export abstract class Labeler { public pushAgent?: AtpAgent constructor( protected ctx: { - db: Database + db: PrimaryDatabase idResolver: IdResolver cfg: IndexerConfig backgroundQueue: BackgroundQueue diff --git a/packages/bsky/src/labeler/hive.ts b/packages/bsky/src/labeler/hive.ts index b40de2a2400..3088504ae83 100644 --- a/packages/bsky/src/labeler/hive.ts +++ b/packages/bsky/src/labeler/hive.ts @@ -4,7 +4,7 @@ import { CID } from 'multiformats/cid' import { IdResolver } from '@atproto/identity' import { Labeler } from './base' import { keywordLabeling } from './util' -import Database from '../db' +import { PrimaryDatabase } from '../db' import { BackgroundQueue } from '../background' import { IndexerConfig } from '../indexer/config' import { retryHttp } from '../util/retry' @@ -20,7 +20,7 @@ export class HiveLabeler extends Labeler { constructor( hiveApiKey: string, protected ctx: { - db: Database + db: PrimaryDatabase idResolver: IdResolver cfg: IndexerConfig backgroundQueue: BackgroundQueue @@ -50,7 +50,12 @@ export class HiveLabeler extends Labeler { } async makeHiveReq(did: string, cid: CID): Promise { - const { stream } = await resolveBlob(did, cid, this.ctx) + const { stream } = await resolveBlob( + did, + cid, + this.ctx.db, + this.ctx.idResolver, + ) const form = new FormData() form.append('media', stream) const { data } = await axios.post(HIVE_ENDPOINT, form, { diff --git a/packages/bsky/src/labeler/keyword.ts b/packages/bsky/src/labeler/keyword.ts index ddd3dfecda8..60f002fa6c3 100644 --- a/packages/bsky/src/labeler/keyword.ts +++ b/packages/bsky/src/labeler/keyword.ts @@ -1,4 +1,4 @@ -import Database from '../db' +import { PrimaryDatabase } from '../db' import { Labeler } from './base' import { getFieldsFromRecord, keywordLabeling } from './util' import { IdResolver } from '@atproto/identity' @@ -11,7 +11,7 @@ export class KeywordLabeler extends Labeler { constructor( protected ctx: { - db: Database + db: PrimaryDatabase idResolver: IdResolver cfg: IndexerConfig backgroundQueue: BackgroundQueue diff --git a/packages/bsky/src/services/actor/index.ts b/packages/bsky/src/services/actor/index.ts index ccb86d944e4..ef27b780a66 100644 --- a/packages/bsky/src/services/actor/index.ts +++ b/packages/bsky/src/services/actor/index.ts @@ -1,5 +1,5 @@ import { sql } from 'kysely' -import Database from '../../db' +import { Database } from '../../db' import { DbRef, notSoftDeletedClause } from '../../db/util' import { ActorViews } from './views' import { ImageUriBuilder } from '../../image/uri' diff --git a/packages/bsky/src/services/actor/views.ts b/packages/bsky/src/services/actor/views.ts index 1313b0a66a6..6794b823fa1 100644 --- a/packages/bsky/src/services/actor/views.ts +++ b/packages/bsky/src/services/actor/views.ts @@ -6,7 +6,7 @@ import { ProfileView, ProfileViewBasic, } from '../../lexicon/types/app/bsky/actor/defs' -import Database from '../../db' +import { Database } from '../../db' import { noMatch, notSoftDeletedClause } from '../../db/util' import { Actor } from '../../db/tables/actor' import { ImageUriBuilder } from '../../image/uri' diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index be8983837c7..09fa32e74d2 100644 --- a/packages/bsky/src/services/feed/index.ts +++ b/packages/bsky/src/services/feed/index.ts @@ -3,7 +3,7 @@ import { AtUri } from '@atproto/uri' import { dedupeStrs } from '@atproto/common' import { INVALID_HANDLE } from '@atproto/identifier' import { jsonStringToLex } from '@atproto/lexicon' -import Database from '../../db' +import { Database } from '../../db' import { countAll, noMatch, diff --git a/packages/bsky/src/services/feed/views.ts b/packages/bsky/src/services/feed/views.ts index 0f24b6e8f2e..c040f96522c 100644 --- a/packages/bsky/src/services/feed/views.ts +++ b/packages/bsky/src/services/feed/views.ts @@ -1,4 +1,4 @@ -import Database from '../../db' +import { Database } from '../../db' import { FeedViewPost, GeneratorView, diff --git a/packages/bsky/src/services/graph/index.ts b/packages/bsky/src/services/graph/index.ts index a33c574ecc0..4c05dc7cecd 100644 --- a/packages/bsky/src/services/graph/index.ts +++ b/packages/bsky/src/services/graph/index.ts @@ -1,4 +1,4 @@ -import Database from '../../db' +import { Database } from '../../db' import { ImageUriBuilder } from '../../image/uri' import { ProfileView } from '../../lexicon/types/app/bsky/actor/defs' import { List } from '../../db/tables/list' @@ -19,8 +19,9 @@ export class GraphService { createdAt?: Date }) { const { subjectDid, mutedByDid, createdAt = new Date() } = info - await this.db.db - .insertInto('mute') + await this.db + .asPrimary() + .db.insertInto('mute') .values({ subjectDid, mutedByDid, @@ -32,8 +33,9 @@ export class GraphService { async unmuteActor(info: { subjectDid: string; mutedByDid: string }) { const { subjectDid, mutedByDid } = info - await this.db.db - .deleteFrom('mute') + await this.db + .asPrimary() + .db.deleteFrom('mute') .where('subjectDid', '=', subjectDid) .where('mutedByDid', '=', mutedByDid) .execute() @@ -45,8 +47,9 @@ export class GraphService { createdAt?: Date }) { const { list, mutedByDid, createdAt = new Date() } = info - await this.db.db - .insertInto('list_mute') + await this.db + .asPrimary() + .db.insertInto('list_mute') .values({ listUri: list, mutedByDid, @@ -58,8 +61,9 @@ export class GraphService { async unmuteActorList(info: { list: string; mutedByDid: string }) { const { list, mutedByDid } = info - await this.db.db - .deleteFrom('list_mute') + await this.db + .asPrimary() + .db.deleteFrom('list_mute') .where('listUri', '=', list) .where('mutedByDid', '=', mutedByDid) .execute() diff --git a/packages/bsky/src/services/index.ts b/packages/bsky/src/services/index.ts index 92c3e9b1f29..c3fe47e6eff 100644 --- a/packages/bsky/src/services/index.ts +++ b/packages/bsky/src/services/index.ts @@ -1,4 +1,4 @@ -import Database from '../db' +import { Database, PrimaryDatabase } from '../db' import { ImageUriBuilder } from '../image/uri' import { ActorService } from './actor' import { FeedService } from './feed' @@ -27,8 +27,10 @@ export type Services = { actor: FromDb feed: FromDb graph: FromDb - moderation: FromDb + moderation: FromDbPrimary label: FromDb } type FromDb = (db: Database) => T + +type FromDbPrimary = (db: PrimaryDatabase) => T diff --git a/packages/bsky/src/services/indexing/index.ts b/packages/bsky/src/services/indexing/index.ts index 9f73580b5af..120ee396e44 100644 --- a/packages/bsky/src/services/indexing/index.ts +++ b/packages/bsky/src/services/indexing/index.ts @@ -13,7 +13,7 @@ import { AtUri } from '@atproto/uri' import { IdResolver, getPds } from '@atproto/identity' import { DAY, chunkArray } from '@atproto/common' import { ValidationError } from '@atproto/lexicon' -import Database from '../../db' +import { PrimaryDatabase } from '../../db' import * as Post from './plugins/post' import * as Like from './plugins/like' import * as Repost from './plugins/repost' @@ -43,7 +43,7 @@ export class IndexingService { } constructor( - public db: Database, + public db: PrimaryDatabase, public idResolver: IdResolver, public labeler: Labeler, public backgroundQueue: BackgroundQueue, @@ -61,7 +61,7 @@ export class IndexingService { } } - transact(txn: Database) { + transact(txn: PrimaryDatabase) { txn.assertTransaction() return new IndexingService( txn, @@ -76,7 +76,7 @@ export class IndexingService { labeler: Labeler, backgroundQueue: BackgroundQueue, ) { - return (db: Database) => + return (db: PrimaryDatabase) => new IndexingService(db, idResolver, labeler, backgroundQueue) } diff --git a/packages/bsky/src/services/indexing/plugins/block.ts b/packages/bsky/src/services/indexing/plugins/block.ts index 1d3c3850c9d..0686d1e6889 100644 --- a/packages/bsky/src/services/indexing/plugins/block.ts +++ b/packages/bsky/src/services/indexing/plugins/block.ts @@ -6,7 +6,7 @@ import * as lex from '../../../lexicon/lexicons' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' import { toSimplifiedISOSafe } from '../util' -import Database from '../../../db' +import { PrimaryDatabase } from '../../../db' import { BackgroundQueue } from '../../../background' const lexId = lex.ids.AppBskyGraphBlock @@ -72,7 +72,7 @@ const notifsForDelete = () => { export type PluginType = RecordProcessor export const makePlugin = ( - db: Database, + db: PrimaryDatabase, backgroundQueue: BackgroundQueue, ): PluginType => { return new RecordProcessor(db, backgroundQueue, { diff --git a/packages/bsky/src/services/indexing/plugins/feed-generator.ts b/packages/bsky/src/services/indexing/plugins/feed-generator.ts index 18812f21b4b..38da130a270 100644 --- a/packages/bsky/src/services/indexing/plugins/feed-generator.ts +++ b/packages/bsky/src/services/indexing/plugins/feed-generator.ts @@ -3,7 +3,7 @@ import { AtUri } from '@atproto/uri' import { CID } from 'multiformats/cid' import * as FeedGenerator from '../../../lexicon/types/app/bsky/feed/generator' import * as lex from '../../../lexicon/lexicons' -import Database from '../../../db' +import { PrimaryDatabase } from '../../../db' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import { BackgroundQueue } from '../../../background' import RecordProcessor from '../processor' @@ -71,7 +71,7 @@ export type PluginType = RecordProcessor< > export const makePlugin = ( - db: Database, + db: PrimaryDatabase, backgroundQueue: BackgroundQueue, ): PluginType => { return new RecordProcessor(db, backgroundQueue, { diff --git a/packages/bsky/src/services/indexing/plugins/follow.ts b/packages/bsky/src/services/indexing/plugins/follow.ts index eca46e4be36..647fd707296 100644 --- a/packages/bsky/src/services/indexing/plugins/follow.ts +++ b/packages/bsky/src/services/indexing/plugins/follow.ts @@ -6,7 +6,7 @@ import * as lex from '../../../lexicon/lexicons' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' import { toSimplifiedISOSafe } from '../util' -import Database from '../../../db' +import { PrimaryDatabase } from '../../../db' import { countAll, excluded } from '../../../db/util' import { BackgroundQueue } from '../../../background' @@ -119,7 +119,7 @@ const updateAggregates = async (db: DatabaseSchema, follow: IndexedFollow) => { export type PluginType = RecordProcessor export const makePlugin = ( - db: Database, + db: PrimaryDatabase, backgroundQueue: BackgroundQueue, ): PluginType => { return new RecordProcessor(db, backgroundQueue, { diff --git a/packages/bsky/src/services/indexing/plugins/like.ts b/packages/bsky/src/services/indexing/plugins/like.ts index d92cf2e6b2e..34ea4e0f6ad 100644 --- a/packages/bsky/src/services/indexing/plugins/like.ts +++ b/packages/bsky/src/services/indexing/plugins/like.ts @@ -7,7 +7,7 @@ import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' import { toSimplifiedISOSafe } from '../util' import { countAll, excluded } from '../../../db/util' -import Database from '../../../db' +import { PrimaryDatabase } from '../../../db' import { BackgroundQueue } from '../../../background' const lexId = lex.ids.AppBskyFeedLike @@ -105,7 +105,7 @@ const updateAggregates = async (db: DatabaseSchema, like: IndexedLike) => { export type PluginType = RecordProcessor export const makePlugin = ( - db: Database, + db: PrimaryDatabase, backgroundQueue: BackgroundQueue, ): PluginType => { return new RecordProcessor(db, backgroundQueue, { diff --git a/packages/bsky/src/services/indexing/plugins/list-item.ts b/packages/bsky/src/services/indexing/plugins/list-item.ts index f28fc8bf62c..a396858412c 100644 --- a/packages/bsky/src/services/indexing/plugins/list-item.ts +++ b/packages/bsky/src/services/indexing/plugins/list-item.ts @@ -7,7 +7,7 @@ import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' import { toSimplifiedISOSafe } from '../util' import { InvalidRequestError } from '@atproto/xrpc-server' -import Database from '../../../db' +import { PrimaryDatabase } from '../../../db' import { BackgroundQueue } from '../../../background' const lexId = lex.ids.AppBskyGraphListitem @@ -80,7 +80,7 @@ const notifsForDelete = () => { export type PluginType = RecordProcessor export const makePlugin = ( - db: Database, + db: PrimaryDatabase, backgroundQueue: BackgroundQueue, ): PluginType => { return new RecordProcessor(db, backgroundQueue, { diff --git a/packages/bsky/src/services/indexing/plugins/list.ts b/packages/bsky/src/services/indexing/plugins/list.ts index 9ba8444eaf7..38bdc38a9ce 100644 --- a/packages/bsky/src/services/indexing/plugins/list.ts +++ b/packages/bsky/src/services/indexing/plugins/list.ts @@ -6,7 +6,7 @@ import * as lex from '../../../lexicon/lexicons' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' import { toSimplifiedISOSafe } from '../util' -import Database from '../../../db' +import { PrimaryDatabase } from '../../../db' import { BackgroundQueue } from '../../../background' const lexId = lex.ids.AppBskyGraphList @@ -68,7 +68,7 @@ const notifsForDelete = () => { export type PluginType = RecordProcessor export const makePlugin = ( - db: Database, + db: PrimaryDatabase, backgroundQueue: BackgroundQueue, ): PluginType => { return new RecordProcessor(db, backgroundQueue, { diff --git a/packages/bsky/src/services/indexing/plugins/post.ts b/packages/bsky/src/services/indexing/plugins/post.ts index a8e9a5cf988..79a373e9174 100644 --- a/packages/bsky/src/services/indexing/plugins/post.ts +++ b/packages/bsky/src/services/indexing/plugins/post.ts @@ -15,7 +15,7 @@ import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' import { Notification } from '../../../db/tables/notification' import { toSimplifiedISOSafe } from '../util' -import Database from '../../../db' +import { PrimaryDatabase } from '../../../db' import { countAll, excluded } from '../../../db/util' import { BackgroundQueue } from '../../../background' import { getAncestorsAndSelfQb, getDescendentsQb } from '../../util/post' @@ -354,7 +354,7 @@ const updateAggregates = async (db: DatabaseSchema, postIdx: IndexedPost) => { export type PluginType = RecordProcessor export const makePlugin = ( - db: Database, + db: PrimaryDatabase, backgroundQueue: BackgroundQueue, ): PluginType => { return new RecordProcessor(db, backgroundQueue, { diff --git a/packages/bsky/src/services/indexing/plugins/profile.ts b/packages/bsky/src/services/indexing/plugins/profile.ts index 8f67aad0ae2..5024c93ab02 100644 --- a/packages/bsky/src/services/indexing/plugins/profile.ts +++ b/packages/bsky/src/services/indexing/plugins/profile.ts @@ -4,7 +4,7 @@ import * as Profile from '../../../lexicon/types/app/bsky/actor/profile' import * as lex from '../../../lexicon/lexicons' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' -import Database from '../../../db' +import { PrimaryDatabase } from '../../../db' import { BackgroundQueue } from '../../../background' const lexId = lex.ids.AppBskyActorProfile @@ -63,7 +63,7 @@ const notifsForDelete = () => { export type PluginType = RecordProcessor export const makePlugin = ( - db: Database, + db: PrimaryDatabase, backgroundQueue: BackgroundQueue, ): PluginType => { return new RecordProcessor(db, backgroundQueue, { diff --git a/packages/bsky/src/services/indexing/plugins/repost.ts b/packages/bsky/src/services/indexing/plugins/repost.ts index 679d94e0972..6e806804dda 100644 --- a/packages/bsky/src/services/indexing/plugins/repost.ts +++ b/packages/bsky/src/services/indexing/plugins/repost.ts @@ -6,7 +6,7 @@ import * as lex from '../../../lexicon/lexicons' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' import { toSimplifiedISOSafe } from '../util' -import Database from '../../../db' +import { PrimaryDatabase } from '../../../db' import { countAll, excluded } from '../../../db/util' import { BackgroundQueue } from '../../../background' @@ -130,7 +130,7 @@ const updateAggregates = async (db: DatabaseSchema, repost: IndexedRepost) => { export type PluginType = RecordProcessor export const makePlugin = ( - db: Database, + db: PrimaryDatabase, backgroundQueue: BackgroundQueue, ): PluginType => { return new RecordProcessor(db, backgroundQueue, { diff --git a/packages/bsky/src/services/indexing/processor.ts b/packages/bsky/src/services/indexing/processor.ts index fcce53fb15c..3321ad5b634 100644 --- a/packages/bsky/src/services/indexing/processor.ts +++ b/packages/bsky/src/services/indexing/processor.ts @@ -6,7 +6,7 @@ import DatabaseSchema from '../../db/database-schema' import { lexicons } from '../../lexicon/lexicons' import { Notification } from '../../db/tables/notification' import { chunkArray } from '@atproto/common' -import Database from '../../db' +import { PrimaryDatabase } from '../../db' import { BackgroundQueue } from '../../background' // @NOTE re: insertions and deletions. Due to how record updates are handled, @@ -40,7 +40,7 @@ export class RecordProcessor { collection: string db: DatabaseSchema constructor( - private appDb: Database, + private appDb: PrimaryDatabase, private backgroundQueue: BackgroundQueue, private params: RecordProcessorParams, ) { @@ -209,7 +209,7 @@ export class RecordProcessor { async handleNotifs(op: { deleted?: S; inserted?: S }) { let notifs: Notif[] = [] - const runOnCommit: ((db: Database) => Promise)[] = [] + const runOnCommit: ((db: PrimaryDatabase) => Promise)[] = [] if (op.deleted) { const forDelete = this.params.notifsForDelete( op.deleted, diff --git a/packages/bsky/src/services/label/index.ts b/packages/bsky/src/services/label/index.ts index 9d80c2fca53..08970395795 100644 --- a/packages/bsky/src/services/label/index.ts +++ b/packages/bsky/src/services/label/index.ts @@ -1,6 +1,6 @@ import { sql } from 'kysely' import { AtUri } from '@atproto/uri' -import Database from '../../db' +import { Database } from '../../db' import { Label, isSelfLabels } from '../../lexicon/types/com/atproto/label/defs' import { ids } from '../../lexicon/lexicons' import { toSimplifiedISOSafe } from '../indexing/util' @@ -50,8 +50,9 @@ export class LabelService { })) const { ref } = this.db.db.dynamic const excluded = (col: string) => ref(`excluded.${col}`) - await this.db.db - .insertInto('label') + await this.db + .asPrimary() + .db.insertInto('label') .values(dbVals) .onConflict((oc) => oc.columns(['src', 'uri', 'cid', 'val']).doUpdateSet({ diff --git a/packages/bsky/src/services/moderation/index.ts b/packages/bsky/src/services/moderation/index.ts index 4f5b2b8d326..241aa9f5439 100644 --- a/packages/bsky/src/services/moderation/index.ts +++ b/packages/bsky/src/services/moderation/index.ts @@ -2,7 +2,7 @@ import { Selectable, sql } from 'kysely' import { CID } from 'multiformats/cid' import { AtUri } from '@atproto/uri' import { InvalidRequestError } from '@atproto/xrpc-server' -import Database from '../../db' +import { PrimaryDatabase } from '../../db' import { ModerationAction, ModerationReport } from '../../db/tables/moderation' import { ModerationViews } from './views' import { ImageUriBuilder } from '../../image/uri' @@ -10,7 +10,7 @@ import { ImageInvalidator } from '../../image/invalidator' export class ModerationService { constructor( - public db: Database, + public db: PrimaryDatabase, public imgUriBuilder: ImageUriBuilder, public imgInvalidator: ImageInvalidator, ) {} @@ -19,7 +19,7 @@ export class ModerationService { imgUriBuilder: ImageUriBuilder, imgInvalidator: ImageInvalidator, ) { - return (db: Database) => + return (db: PrimaryDatabase) => new ModerationService(db, imgUriBuilder, imgInvalidator) } diff --git a/packages/bsky/src/services/moderation/views.ts b/packages/bsky/src/services/moderation/views.ts index ac5130b5283..9ce0cfbb44a 100644 --- a/packages/bsky/src/services/moderation/views.ts +++ b/packages/bsky/src/services/moderation/views.ts @@ -3,7 +3,7 @@ import { ArrayEl } from '@atproto/common' import { AtUri } from '@atproto/uri' import { INVALID_HANDLE } from '@atproto/identifier' import { BlobRef, jsonStringToLex } from '@atproto/lexicon' -import Database from '../../db' +import { Database } from '../../db' import { Actor } from '../../db/tables/actor' import { Record as RecordRow } from '../../db/tables/record' import { ModerationAction } from '../../db/tables/moderation' diff --git a/packages/bsky/src/services/util/search.ts b/packages/bsky/src/services/util/search.ts index e5b4b6b341e..9dfebadb613 100644 --- a/packages/bsky/src/services/util/search.ts +++ b/packages/bsky/src/services/util/search.ts @@ -1,6 +1,6 @@ import { sql } from 'kysely' import { InvalidRequestError } from '@atproto/xrpc-server' -import Database from '../../db' +import { Database } from '../../db' import { notSoftDeletedClause, DbRef, AnyQb } from '../../db/util' import { GenericKeyset, paginate } from '../../db/pagination' diff --git a/packages/bsky/tests/algos/whats-hot.test.ts b/packages/bsky/tests/algos/whats-hot.test.ts index dc26f51528b..23cac215dda 100644 --- a/packages/bsky/tests/algos/whats-hot.test.ts +++ b/packages/bsky/tests/algos/whats-hot.test.ts @@ -77,13 +77,16 @@ describe.skip('algo whats-hot', () => { await network.bsky.processAll() // move the 3rd post 5 hours into the past to check gravity - await network.bsky.ctx.db.db - .updateTable('post') + await network.bsky.ctx.db + .getPrimary() + .db.updateTable('post') .where('uri', '=', three.ref.uriStr) .set({ indexedAt: new Date(Date.now() - 5 * HOUR).toISOString() }) .execute() - await network.bsky.ctx.db.refreshMaterializedView('algo_whats_hot_view') + await network.bsky.ctx.db + .getPrimary() + .refreshMaterializedView('algo_whats_hot_view') const res = await agent.api.app.bsky.feed.getFeed( { feed: feedUri }, diff --git a/packages/bsky/tests/db.test.ts b/packages/bsky/tests/db.test.ts index 26a80be69c4..565f07a0e73 100644 --- a/packages/bsky/tests/db.test.ts +++ b/packages/bsky/tests/db.test.ts @@ -2,17 +2,18 @@ import { once } from 'events' import { wait } from '@atproto/common' import { TestNetwork } from '@atproto/dev-env' import { Database } from '../src' +import { PrimaryDatabase } from '../src/db' import { Leader } from '../src/db/leader' describe('db', () => { let network: TestNetwork - let db: Database + let db: PrimaryDatabase beforeAll(async () => { network = await TestNetwork.create({ dbPostgresSchema: 'bsky_db', }) - db = network.bsky.ctx.db + db = network.bsky.ctx.db.getPrimary() }) afterAll(async () => { diff --git a/packages/bsky/tests/did-cache.test.ts b/packages/bsky/tests/did-cache.test.ts index 659c6eb3bcf..be7544a4a3b 100644 --- a/packages/bsky/tests/did-cache.test.ts +++ b/packages/bsky/tests/did-cache.test.ts @@ -84,7 +84,7 @@ describe('did cache', () => { }) it('accurately reports expired dids & refreshes the cache', async () => { - const didCache = new DidSqlCache(network.bsky.ctx.db, 1, 60000) + const didCache = new DidSqlCache(network.bsky.ctx.db.getPrimary(), 1, 60000) const shortCacheResolver = new IdResolver({ plcUrl: network.bsky.ctx.cfg.didPlcUrl, didCache, @@ -113,7 +113,7 @@ describe('did cache', () => { }) it('does not return expired dids & refreshes the cache', async () => { - const didCache = new DidSqlCache(network.bsky.ctx.db, 0, 1) + const didCache = new DidSqlCache(network.bsky.ctx.db.getPrimary(), 0, 1) const shortExpireResolver = new IdResolver({ plcUrl: network.bsky.ctx.cfg.didPlcUrl, didCache, diff --git a/packages/bsky/tests/duplicate-records.test.ts b/packages/bsky/tests/duplicate-records.test.ts index ec66b7d30dc..5aab5d0a9c7 100644 --- a/packages/bsky/tests/duplicate-records.test.ts +++ b/packages/bsky/tests/duplicate-records.test.ts @@ -3,13 +3,14 @@ import { cidForCbor, TID } from '@atproto/common' import { WriteOpAction } from '@atproto/repo' import { TestNetwork } from '@atproto/dev-env' import { Database } from '../src' +import { PrimaryDatabase } from '../src/db' import * as lex from '../src/lexicon/lexicons' import { Services } from '../src/indexer/services' describe('duplicate record', () => { let network: TestNetwork let did: string - let db: Database + let db: PrimaryDatabase let services: Services beforeAll(async () => { diff --git a/packages/bsky/tests/handle-invalidation.test.ts b/packages/bsky/tests/handle-invalidation.test.ts index b2387b739ff..3b9ae789265 100644 --- a/packages/bsky/tests/handle-invalidation.test.ts +++ b/packages/bsky/tests/handle-invalidation.test.ts @@ -46,8 +46,9 @@ describe('handle invalidation', () => { const backdateIndexedAt = async (did: string) => { const TWO_DAYS_AGO = new Date(Date.now() - 2 * DAY).toISOString() - await network.bsky.ctx.db.db - .updateTable('actor') + await network.bsky.ctx.db + .getPrimary() + .db.updateTable('actor') .set({ indexedAt: TWO_DAYS_AGO }) .where('did', '=', did) .execute() diff --git a/packages/bsky/tests/indexing.test.ts b/packages/bsky/tests/indexing.test.ts index bb0f9129433..14dd7e65ef4 100644 --- a/packages/bsky/tests/indexing.test.ts +++ b/packages/bsky/tests/indexing.test.ts @@ -4,7 +4,6 @@ import { cidForCbor, TID } from '@atproto/common' import * as pdsRepo from '@atproto/pds/src/repo/prepare' import { WriteOpAction } from '@atproto/repo' import { AtUri } from '@atproto/uri' -import { Client } from '@did-plc/lib' import AtpAgent, { AppBskyActorProfile, AppBskyFeedPost, diff --git a/packages/bsky/tests/moderation.test.ts b/packages/bsky/tests/moderation.test.ts index 7c7e50797ca..8b370ab4848 100644 --- a/packages/bsky/tests/moderation.test.ts +++ b/packages/bsky/tests/moderation.test.ts @@ -806,7 +806,7 @@ describe('moderation', () => { it('negates an existing label and reverses.', async () => { const { ctx } = network.bsky const post = sc.posts[sc.dids.bob][0].ref - const labelingService = ctx.services.label(ctx.db) + const labelingService = ctx.services.label(ctx.db.getPrimary()) await labelingService.formatAndCreate( ctx.cfg.labelerDid, post.uriStr, @@ -836,7 +836,7 @@ describe('moderation', () => { 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) + const labelingService = ctx.services.label(ctx.db.getPrimary()) const action = await actionWithLabels({ negateLabelVals: ['bears'], subject: { @@ -879,7 +879,7 @@ describe('moderation', () => { it('no-ops when creating an existing label and reverses.', async () => { const { ctx } = network.bsky const post = sc.posts[sc.dids.bob][0].ref - const labelingService = ctx.services.label(ctx.db) + const labelingService = ctx.services.label(ctx.db.getPrimary()) await labelingService.formatAndCreate( ctx.cfg.labelerDid, post.uriStr, @@ -918,7 +918,7 @@ describe('moderation', () => { it('creates and negates labels on a repo and reverses.', async () => { const { ctx } = network.bsky - const labelingService = ctx.services.label(ctx.db) + const labelingService = ctx.services.label(ctx.db.getPrimary()) await labelingService.formatAndCreate( ctx.cfg.labelerDid, sc.dids.bob, diff --git a/packages/bsky/tests/server.test.ts b/packages/bsky/tests/server.test.ts index 4974a2ea74d..be2f1c0213e 100644 --- a/packages/bsky/tests/server.test.ts +++ b/packages/bsky/tests/server.test.ts @@ -21,7 +21,7 @@ describe('server', () => { await basicSeed(sc) await network.processAll() alice = sc.dids.alice - db = network.bsky.ctx.db + db = network.bsky.ctx.db.getPrimary() }) afterAll(async () => { diff --git a/packages/bsky/tests/subscription/repo.test.ts b/packages/bsky/tests/subscription/repo.test.ts index 23e32d9c130..43c1287ba95 100644 --- a/packages/bsky/tests/subscription/repo.test.ts +++ b/packages/bsky/tests/subscription/repo.test.ts @@ -34,7 +34,7 @@ describe('sync', () => { }) it('indexes permit history being replayed.', async () => { - const { db } = ctx + const db = ctx.db.getPrimary() // Generate some modifications and dupes const { alice, bob, carol, dan } = sc.dids @@ -108,7 +108,7 @@ describe('sync', () => { await network.pds.ctx.sequencerLeader?.isCaughtUp() await network.processAll() // confirm jack was indexed as an actor despite the bad event - const actors = await dumpTable(ctx.db, 'actor', ['did']) + const actors = await dumpTable(ctx.db.getPrimary(), 'actor', ['did']) expect(actors.map((a) => a.handle)).toContain('jack.test') RepoService.prototype.afterWriteProcessing = afterWriteProcessingOriginal }) diff --git a/packages/bsky/tests/views/actor-search.test.ts b/packages/bsky/tests/views/actor-search.test.ts index b3e4fc15ffb..77f657a9bf6 100644 --- a/packages/bsky/tests/views/actor-search.test.ts +++ b/packages/bsky/tests/views/actor-search.test.ts @@ -25,7 +25,7 @@ describe('pds actor search views', () => { await usersBulkSeed(sc) // Skip did/handle resolution for expediency - const { db } = network.bsky.ctx + const db = network.bsky.ctx.db.getPrimary() const now = new Date().toISOString() await db.db .insertInto('actor') diff --git a/packages/bsky/tests/views/admin/repo-search.test.ts b/packages/bsky/tests/views/admin/repo-search.test.ts index 1474a562e53..1e965320279 100644 --- a/packages/bsky/tests/views/admin/repo-search.test.ts +++ b/packages/bsky/tests/views/admin/repo-search.test.ts @@ -23,7 +23,7 @@ describe('pds admin repo search views', () => { await usersBulkSeed(sc) // Skip did/handle resolution for expediency - const { db } = network.bsky.ctx + const db = network.bsky.ctx.db.getPrimary() const now = new Date().toISOString() await db.db .insertInto('actor') diff --git a/packages/bsky/tests/views/suggestions.test.ts b/packages/bsky/tests/views/suggestions.test.ts index 9272a9514cd..e69bd5e377e 100644 --- a/packages/bsky/tests/views/suggestions.test.ts +++ b/packages/bsky/tests/views/suggestions.test.ts @@ -25,8 +25,9 @@ describe('pds user search views', () => { { did: sc.dids.carol, order: 2 }, { did: sc.dids.dan, order: 3 }, ] - await network.bsky.ctx.db.db - .insertInto('suggested_follow') + await network.bsky.ctx.db + .getPrimary() + .db.insertInto('suggested_follow') .values(suggestions) .execute() }) diff --git a/packages/bsky/tests/views/timeline.test.ts b/packages/bsky/tests/views/timeline.test.ts index e3111ebc5b8..e7db746c7f3 100644 --- a/packages/bsky/tests/views/timeline.test.ts +++ b/packages/bsky/tests/views/timeline.test.ts @@ -36,7 +36,7 @@ describe('timeline views', () => { const labelPostA = sc.posts[bob][0].ref const labelPostB = sc.posts[carol][0].ref await network.bsky.ctx.services - .label(network.bsky.ctx.db) + .label(network.bsky.ctx.db.getPrimary()) .formatAndCreate( network.bsky.ctx.cfg.labelerDid, labelPostA.uriStr, @@ -44,7 +44,7 @@ describe('timeline views', () => { { create: ['kind'] }, ) await network.bsky.ctx.services - .label(network.bsky.ctx.db) + .label(network.bsky.ctx.db.getPrimary()) .formatAndCreate( network.bsky.ctx.cfg.labelerDid, labelPostB.uriStr, diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index abb9725f6a6..8445881244e 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -51,15 +51,18 @@ export class TestBsky { }) // shared across server, ingester, and indexer in order to share pool, avoid too many pg connections. - const db = bsky.Database.postgres({ - url: cfg.dbPostgresUrl, + const db = new bsky.DatabaseCoordinator({ schema: cfg.dbPostgresSchema, - poolSize: 10, + primary: { + url: cfg.dbPrimaryPostgresUrl, + poolSize: 10, + }, + replicas: [], }) // Separate migration db in case migration changes some connection state that we need in the tests, e.g. "alter database ... set ..." - const migrationDb = bsky.Database.postgres({ - url: cfg.dbPostgresUrl, + const migrationDb = new bsky.PrimaryDatabase({ + url: cfg.dbPrimaryPostgresUrl, schema: cfg.dbPostgresSchema, }) if (cfg.migration) { @@ -70,7 +73,11 @@ export class TestBsky { await migrationDb.close() // api server - const server = bsky.BskyAppView.create({ db, config, algos: cfg.algos }) + const server = bsky.BskyAppView.create({ + db, + config, + algos: cfg.algos, + }) // indexer const ns = cfg.dbPostgresSchema ? await randomIntFromSeed(cfg.dbPostgresSchema, 10000) @@ -81,7 +88,7 @@ export class TestBsky { didCacheMaxTTL: DAY, labelerDid: 'did:example:labeler', redisHost: cfg.redisHost, - dbPostgresUrl: cfg.dbPostgresUrl, + dbPostgresUrl: cfg.dbPrimaryPostgresUrl, dbPostgresSchema: cfg.dbPostgresSchema, didPlcUrl: cfg.plcUrl, labelerKeywords: { label_me: 'test-label', label_me_2: 'test-label-2' }, @@ -96,14 +103,14 @@ export class TestBsky { }) const indexer = bsky.BskyIndexer.create({ cfg: indexerCfg, - db, + db: db.getPrimary(), redis: indexerRedis, }) // ingester const ingesterCfg = new bsky.IngesterConfig({ version: '0.0.0', redisHost: cfg.redisHost, - dbPostgresUrl: cfg.dbPostgresUrl, + dbPostgresUrl: cfg.dbPrimaryPostgresUrl, dbPostgresSchema: cfg.dbPostgresSchema, repoProvider: cfg.repoProvider, ingesterNamespace: `ns${ns}`, @@ -117,7 +124,7 @@ export class TestBsky { }) const ingester = bsky.BskyIngester.create({ cfg: ingesterCfg, - db, + db: db.getPrimary(), redis: ingesterRedis, }) await ingester.start() @@ -194,7 +201,7 @@ export async function getIngester( ingesterNamespace: `ns${ns}`, ...config, }) - const db = bsky.Database.postgres({ + const db = new bsky.PrimaryDatabase({ url: cfg.dbPostgresUrl, schema: cfg.dbPostgresSchema, }) @@ -231,7 +238,7 @@ export async function getIndexers( indexerNamespace: `ns${ns}`, ...config, } - const db = bsky.Database.postgres({ + const db = new bsky.PrimaryDatabase({ url: baseCfg.dbPostgresUrl, schema: baseCfg.dbPostgresSchema, }) diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index 4daf69a541d..39d9041e031 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -37,7 +37,7 @@ export class TestNetwork extends TestNetworkNoAppView { plcUrl: plc.url, repoProvider: `ws://localhost:${pdsPort}`, dbPostgresSchema: `appview_${dbPostgresSchema}`, - dbPostgresUrl, + dbPrimaryPostgresUrl: dbPostgresUrl, redisHost, ...params.bsky, }) diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index 654adc4c474..4ea9cb002d8 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -16,7 +16,7 @@ export type PdsConfig = Partial & { export type BskyConfig = Partial & { plcUrl: string repoProvider: string - dbPostgresUrl: string + dbPrimaryPostgresUrl: string redisHost: string migration?: string algos?: bsky.MountedAlgos diff --git a/packages/pds/tests/proxied/views.test.ts b/packages/pds/tests/proxied/views.test.ts index 33166b9de78..066cc780059 100644 --- a/packages/pds/tests/proxied/views.test.ts +++ b/packages/pds/tests/proxied/views.test.ts @@ -63,8 +63,9 @@ describe('proxies view requests', () => { { did: sc.dids.carol, order: 2 }, { did: sc.dids.dan, order: 3 }, ] - await network.bsky.ctx.db.db - .insertInto('suggested_follow') + await network.bsky.ctx.db + .getPrimary() + .db.insertInto('suggested_follow') .values(suggestions) .execute() From d9849b67761c61308749dbcfd8957a2d9daa68a2 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 14 Aug 2023 18:30:44 -0500 Subject: [PATCH 137/237] get it working --- .../src/api/app/bsky/feed/getActorLikes.ts | 84 +++++++++++ .../src/api/app/bsky/feed/getAuthorFeed.ts | 1 + packages/bsky/src/api/index.ts | 2 + packages/bsky/tests/views/actor-likes.test.ts | 73 +++++++++ .../api/app/bsky/feed/getActorLikes.ts | 141 ++++++++++++++++++ .../pds/src/app-view/api/app/bsky/index.ts | 2 + packages/pds/tests/views/actor-likes.test.ts | 73 +++++++++ 7 files changed, 376 insertions(+) create mode 100644 packages/bsky/src/api/app/bsky/feed/getActorLikes.ts create mode 100644 packages/bsky/tests/views/actor-likes.test.ts create mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts create mode 100644 packages/pds/tests/views/actor-likes.test.ts diff --git a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts new file mode 100644 index 00000000000..52b621689eb --- /dev/null +++ b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts @@ -0,0 +1,84 @@ +import { Server } from '../../../../lexicon' +import { FeedKeyset } from '../util/feed' +import { paginate } from '../../../../db/pagination' +import AppContext from '../../../../context' +import { InvalidRequestError } from '@atproto/xrpc-server' +import { setRepoRev } from '../../../util' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.feed.getActorLikes({ + auth: ctx.authOptionalVerifier, + handler: async ({ params, auth, res }) => { + const { actor, limit, cursor } = params + const viewer = auth.credentials.did + const db = ctx.db.db + const { ref } = db.dynamic + + // first verify there is not a block between requester & subject + if (viewer !== null) { + const blocks = await ctx.services.graph(ctx.db).getBlocks(viewer, actor) + if (blocks.blocking) { + throw new InvalidRequestError( + `Requester has blocked actor: ${actor}`, + 'BlockedActor', + ) + } else if (blocks.blockedBy) { + throw new InvalidRequestError( + `Requester is blocked by actor: $${actor}`, + 'BlockedByActor', + ) + } + } + + const actorService = ctx.services.actor(ctx.db) + const feedService = ctx.services.feed(ctx.db) + + let did = '' + if (actor.startsWith('did:')) { + did = actor + } else { + const actorRes = await db + .selectFrom('actor') + .select('did') + .where('handle', '=', actor) + .executeTakeFirst() + if (actorRes) { + did = actorRes?.did + } + } + + let feedItemsQb = feedService + .selectFeedItemQb() + .innerJoin('like', 'like.subject', 'feed_item.uri') + .where('like.creator', '=', did) + + const keyset = new FeedKeyset( + ref('feed_item.sortAt'), + ref('feed_item.cid'), + ) + + feedItemsQb = paginate(feedItemsQb, { + limit, + cursor, + keyset, + }) + + const [feedItems, repoRev] = await Promise.all([ + feedItemsQb.execute(), + // TODO why + actorService.getRepoRev(viewer), + ]) + setRepoRev(res, repoRev) + + const feed = await feedService.hydrateFeed(feedItems, viewer) + + return { + encoding: 'application/json', + body: { + feed, + cursor: keyset.packFromResult(feedItems), + }, + } + }, + }) +} diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index b71cafbec02..056d957b68b 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -90,6 +90,7 @@ export default function (server: Server, ctx: AppContext) { const [feedItems, repoRev] = await Promise.all([ feedItemsQb.execute(), + // TODO why actorService.getRepoRev(viewer), ]) setRepoRev(res, repoRev) diff --git a/packages/bsky/src/api/index.ts b/packages/bsky/src/api/index.ts index 78edf7c51b4..6ecf3495783 100644 --- a/packages/bsky/src/api/index.ts +++ b/packages/bsky/src/api/index.ts @@ -11,6 +11,7 @@ import getFeedSkeleton from './app/bsky/feed/getFeedSkeleton' import getLikes from './app/bsky/feed/getLikes' import getPostThread from './app/bsky/feed/getPostThread' import getPosts from './app/bsky/feed/getPosts' +import getActorLikes from './app/bsky/feed/getActorLikes' import getProfile from './app/bsky/actor/getProfile' import getProfiles from './app/bsky/actor/getProfiles' import getRepostedBy from './app/bsky/feed/getRepostedBy' @@ -64,6 +65,7 @@ export default function (server: Server, ctx: AppContext) { getLikes(server, ctx) getPostThread(server, ctx) getPosts(server, ctx) + getActorLikes(server, ctx) getProfile(server, ctx) getProfiles(server, ctx) getRepostedBy(server, ctx) diff --git a/packages/bsky/tests/views/actor-likes.test.ts b/packages/bsky/tests/views/actor-likes.test.ts new file mode 100644 index 00000000000..9aef63a65b7 --- /dev/null +++ b/packages/bsky/tests/views/actor-likes.test.ts @@ -0,0 +1,73 @@ +import AtpAgent from '@atproto/api' +import { TestNetwork } from '@atproto/dev-env' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' + +describe('bsky actor likes feed views', () => { + let network: TestNetwork + let agent: AtpAgent + let sc: SeedClient + + // account dids, for convenience + let alice: string + let bob: string + let carol: string + let dan: string + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_views_actor_likes', + }) + agent = network.bsky.getClient() + const pdsAgent = network.pds.getClient() + sc = new SeedClient(pdsAgent) + await basicSeed(sc) + await network.processAll() + alice = sc.dids.alice + bob = sc.dids.bob + carol = sc.dids.carol + dan = sc.dids.dan + }) + + afterAll(async () => { + await network.close() + }) + + it('returns posts liked by actor', async () => { + const { + data: { feed: bobLikes }, + } = await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[bob].handle }, + { headers: await network.serviceHeaders(bob) }, + ) + + expect(bobLikes).toHaveLength(3) + + const { + data: { feed: carolLikes }, + } = await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[carol].handle }, + { headers: await network.serviceHeaders(carol) }, + ) + + expect(carolLikes).toHaveLength(2) + + const { + data: { feed: aliceLikes }, + } = await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[alice].handle }, + { headers: await network.serviceHeaders(alice) }, + ) + + expect(aliceLikes).toHaveLength(1) + + const { + data: { feed: danLikes }, + } = await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[dan].handle }, + { headers: await network.serviceHeaders(dan) }, + ) + + expect(danLikes).toHaveLength(1) + }) +}) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts new file mode 100644 index 00000000000..0cac7accdbb --- /dev/null +++ b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts @@ -0,0 +1,141 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' +import { Server } from '../../../../../lexicon' +import { FeedKeyset } from '../util/feed' +import { paginate } from '../../../../../db/pagination' +import AppContext from '../../../../../context' +import { FeedRow } from '../../../../services/feed' +import { OutputSchema } from '../../../../../lexicon/types/app/bsky/feed/getAuthorFeed' +import { handleReadAfterWrite } from '../util/read-after-write' +import { authPassthru } from '../../../../../api/com/atproto/admin/util' +import { LocalRecords } from '../../../../../services/local' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.feed.getActorLikes({ + auth: ctx.accessOrRoleVerifier, + handler: async ({ req, params, auth }) => { + const requester = + auth.credentials.type === 'access' ? auth.credentials.did : null + + if (ctx.canProxyRead(req)) { + const res = await ctx.appviewAgent.api.app.bsky.feed.getActorLikes( + params, + requester + ? await ctx.serviceAuthHeaders(requester) + : authPassthru(req), + ) + if (requester) { + return await handleReadAfterWrite(ctx, requester, res, getAuthorMunge) + } + return { + encoding: 'application/json', + body: res.data, + } + } + + const { actor, limit, cursor } = params + + // for access-based auth, enforce blocks and mutes + if (requester) { + await assertNoBlocks(ctx, { requester, actor }) + } + + const { ref } = ctx.db.db.dynamic + const feedService = ctx.services.appView.feed(ctx.db) + + const userLookupCol = actor.startsWith('did:') + ? 'did_handle.did' + : 'did_handle.handle' + const actorDidQb = ctx.db.db + .selectFrom('did_handle') + .select('did') + .where(userLookupCol, '=', actor) + .limit(1) + + // defaults to posts, reposts, and replies + let feedItemsQb = feedService + .selectFeedItemQb() + .innerJoin('like', 'like.subject', 'feed_item.uri') + .where('like.creator', '=', actorDidQb) + + const keyset = new FeedKeyset( + ref('feed_item.sortAt'), + ref('feed_item.cid'), + ) + + feedItemsQb = paginate(feedItemsQb, { + limit, + cursor, + keyset, + }) + + const feedItems: FeedRow[] = await feedItemsQb.execute() + const feed = await feedService.hydrateFeed(feedItems, requester, { + includeSoftDeleted: auth.credentials.type === 'role', // show takendown content to mods + }) + + return { + encoding: 'application/json', + body: { + feed, + cursor: keyset.packFromResult(feedItems), + }, + } + }, + }) +} + +// throws when there's a block between the two users +async function assertNoBlocks( + ctx: AppContext, + opts: { requester: string; actor: string }, +) { + const { requester, actor } = opts + const graphService = ctx.services.appView.graph(ctx.db) + const blocks = await graphService.getBlocks(requester, actor) + if (blocks.blocking) { + throw new InvalidRequestError( + `Requester has blocked actor: ${actor}`, + 'BlockedActor', + ) + } else if (blocks.blockedBy) { + throw new InvalidRequestError( + `Requester is blocked by actor: $${actor}`, + 'BlockedByActor', + ) + } +} + +const getAuthorMunge = async ( + ctx: AppContext, + original: OutputSchema, + local: LocalRecords, + requester: string, +): Promise => { + const localSrvc = ctx.services.local(ctx.db) + const localProf = local.profile + let feed = original.feed + // first update any out of date profile pictures in feed + if (localProf) { + feed = feed.map((item) => { + if (item.post.author.did === requester) { + return { + ...item, + post: { + ...item.post, + author: localSrvc.updateProfileViewBasic( + item.post.author, + localProf.record, + ), + }, + } + } else { + return item + } + }) + } + feed = await localSrvc.formatAndInsertPostsInFeed(feed, local.posts) + return { + ...original, + feed, + } +} diff --git a/packages/pds/src/app-view/api/app/bsky/index.ts b/packages/pds/src/app-view/api/app/bsky/index.ts index 9d895ea6667..a4055240844 100644 --- a/packages/pds/src/app-view/api/app/bsky/index.ts +++ b/packages/pds/src/app-view/api/app/bsky/index.ts @@ -10,6 +10,7 @@ import getFeed from './feed/getFeed' import getLikes from './feed/getLikes' import getPostThread from './feed/getPostThread' import getPosts from './feed/getPosts' +import getActorLikes from './feed/getActorLikes' import getProfile from './actor/getProfile' import getProfiles from './actor/getProfiles' import getRepostedBy from './feed/getRepostedBy' @@ -43,6 +44,7 @@ export default function (server: Server, ctx: AppContext) { getLikes(server, ctx) getPostThread(server, ctx) getPosts(server, ctx) + getActorLikes(server, ctx) getProfile(server, ctx) getProfiles(server, ctx) getRepostedBy(server, ctx) diff --git a/packages/pds/tests/views/actor-likes.test.ts b/packages/pds/tests/views/actor-likes.test.ts new file mode 100644 index 00000000000..aec63063d0f --- /dev/null +++ b/packages/pds/tests/views/actor-likes.test.ts @@ -0,0 +1,73 @@ +import AtpAgent from '@atproto/api' +import { runTestServer, CloseFn } from '../_util' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' + +describe('pds actor likes feed views', () => { + let agent: AtpAgent + let close: CloseFn + let sc: SeedClient + + // account dids, for convenience + let alice: string + let bob: string + let carol: string + let dan: string + + beforeAll(async () => { + const server = await runTestServer({ + dbPostgresSchema: 'pds_views_actor_likes', + }) + close = server.close + agent = new AtpAgent({ service: server.url }) + sc = new SeedClient(agent) + await basicSeed(sc) + alice = sc.dids.alice + bob = sc.dids.bob + carol = sc.dids.carol + dan = sc.dids.dan + await server.processAll() + }) + + afterAll(async () => { + await close() + }) + + it('returns posts liked by actor', async () => { + const { + data: { feed: bobLikes }, + } = await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[bob].handle }, + { headers: sc.getHeaders(bob) }, + ) + + expect(bobLikes).toHaveLength(3) + + const { + data: { feed: carolLikes }, + } = await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[carol].handle }, + { headers: sc.getHeaders(carol) }, + ) + + expect(carolLikes).toHaveLength(2) + + const { + data: { feed: aliceLikes }, + } = await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[alice].handle }, + { headers: sc.getHeaders(alice) }, + ) + + expect(aliceLikes).toHaveLength(1) + + const { + data: { feed: danLikes }, + } = await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[dan].handle }, + { headers: sc.getHeaders(dan) }, + ) + + expect(danLikes).toHaveLength(1) + }) +}) From 79dcad4cf74658b022017d195c97fc4097dbf797 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 15 Aug 2023 11:25:38 -0500 Subject: [PATCH 138/237] handle mutes --- .../src/api/app/bsky/feed/getActorLikes.ts | 13 ++++++++- .../src/api/app/bsky/feed/getAuthorFeed.ts | 1 - .../api/app/bsky/feed/getActorLikes.ts | 27 +++++++++++++++---- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts index 52b621689eb..1646a9309bd 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts @@ -32,6 +32,7 @@ export default function (server: Server, ctx: AppContext) { const actorService = ctx.services.actor(ctx.db) const feedService = ctx.services.feed(ctx.db) + const graphService = ctx.services.graph(ctx.db) let did = '' if (actor.startsWith('did:')) { @@ -52,6 +53,17 @@ export default function (server: Server, ctx: AppContext) { .innerJoin('like', 'like.subject', 'feed_item.uri') .where('like.creator', '=', did) + if (viewer !== null) { + feedItemsQb = feedItemsQb.where((qb) => + // Hide reposts of muted content + qb + .where('type', '=', 'post') + .orWhere((qb) => + graphService.whereNotMuted(qb, viewer, [ref('post.creator')]), + ), + ) + } + const keyset = new FeedKeyset( ref('feed_item.sortAt'), ref('feed_item.cid'), @@ -65,7 +77,6 @@ export default function (server: Server, ctx: AppContext) { const [feedItems, repoRev] = await Promise.all([ feedItemsQb.execute(), - // TODO why actorService.getRepoRev(viewer), ]) setRepoRev(res, repoRev) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 056d957b68b..b71cafbec02 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -90,7 +90,6 @@ export default function (server: Server, ctx: AppContext) { const [feedItems, repoRev] = await Promise.all([ feedItemsQb.execute(), - // TODO why actorService.getRepoRev(viewer), ]) setRepoRev(res, repoRev) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts index 0cac7accdbb..449a5732e6e 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts @@ -34,14 +34,12 @@ export default function (server: Server, ctx: AppContext) { const { actor, limit, cursor } = params - // for access-based auth, enforce blocks and mutes - if (requester) { - await assertNoBlocks(ctx, { requester, actor }) - } - const { ref } = ctx.db.db.dynamic + const accountService = ctx.services.account(ctx.db) const feedService = ctx.services.appView.feed(ctx.db) + const graphService = ctx.services.appView.graph(ctx.db) + // resolve did const userLookupCol = actor.startsWith('did:') ? 'did_handle.did' : 'did_handle.handle' @@ -57,6 +55,25 @@ export default function (server: Server, ctx: AppContext) { .innerJoin('like', 'like.subject', 'feed_item.uri') .where('like.creator', '=', actorDidQb) + // for access-based auth, enforce blocks and mutes + if (requester) { + await assertNoBlocks(ctx, { requester, actor }) + feedItemsQb = feedItemsQb + .where((qb) => + // hide reposts of muted content + qb + .where('type', '=', 'post') + .orWhere((qb) => + accountService.whereNotMuted(qb, requester, [ + ref('post.creator'), + ]), + ), + ) + .whereNotExists( + graphService.blockQb(requester, [ref('post.creator')]), + ) + } + const keyset = new FeedKeyset( ref('feed_item.sortAt'), ref('feed_item.cid'), From e0e7b39b165607949aea40ed98ca6070053c05fe Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 15 Aug 2023 13:46:58 -0500 Subject: [PATCH 139/237] add block and mute tests --- .../src/api/app/bsky/feed/getActorLikes.ts | 12 +- packages/bsky/tests/views/actor-likes.test.ts | 118 +++++++++++++++++- .../api/app/bsky/feed/getActorLikes.ts | 13 +- packages/pds/tests/views/actor-likes.test.ts | 115 ++++++++++++++++- 4 files changed, 241 insertions(+), 17 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts index 1646a9309bd..aa1652ff0ac 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts @@ -54,14 +54,14 @@ export default function (server: Server, ctx: AppContext) { .where('like.creator', '=', did) if (viewer !== null) { - feedItemsQb = feedItemsQb.where((qb) => - // Hide reposts of muted content - qb - .where('type', '=', 'post') - .orWhere((qb) => + feedItemsQb = feedItemsQb + .where((qb) => + qb.where((qb) => graphService.whereNotMuted(qb, viewer, [ref('post.creator')]), ), - ) + ) + // TODO do we want this? was missing here + .whereNotExists(graphService.blockQb(viewer, [ref('post.creator')])) } const keyset = new FeedKeyset( diff --git a/packages/bsky/tests/views/actor-likes.test.ts b/packages/bsky/tests/views/actor-likes.test.ts index 9aef63a65b7..f942ab7ca27 100644 --- a/packages/bsky/tests/views/actor-likes.test.ts +++ b/packages/bsky/tests/views/actor-likes.test.ts @@ -1,11 +1,16 @@ -import AtpAgent from '@atproto/api' +import AtpAgent, { AtUri } from '@atproto/api' import { TestNetwork } from '@atproto/dev-env' import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' +import { + BlockedByActorError, + BlockedActorError, +} from '@atproto/api/src/client/types/app/bsky/feed/getActorLikes' describe('bsky actor likes feed views', () => { let network: TestNetwork let agent: AtpAgent + let pdsAgent: AtpAgent let sc: SeedClient // account dids, for convenience @@ -19,7 +24,7 @@ describe('bsky actor likes feed views', () => { dbPostgresSchema: 'bsky_views_actor_likes', }) agent = network.bsky.getClient() - const pdsAgent = network.pds.getClient() + pdsAgent = network.pds.getClient() sc = new SeedClient(pdsAgent) await basicSeed(sc) await network.processAll() @@ -70,4 +75,113 @@ describe('bsky actor likes feed views', () => { expect(danLikes).toHaveLength(1) }) + + it('actor blocks viewer', async () => { + const aliceBlockBob = await pdsAgent.api.app.bsky.graph.block.create( + { + repo: alice, // alice blocks bob + }, + { + subject: bob, + createdAt: new Date().toISOString(), + }, + sc.getHeaders(alice), + ) + + try { + await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[alice].handle }, + { headers: await network.serviceHeaders(bob) }, + ) + } catch (e) { + expect(e).toBeInstanceOf(BlockedByActorError) + } + + // unblock + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: alice, rkey: new AtUri(aliceBlockBob.uri).rkey }, + sc.getHeaders(alice), + ) + }) + + it('viewer has blocked actor', async () => { + const bobBlockAlice = await pdsAgent.api.app.bsky.graph.block.create( + { + repo: bob, // alice blocks bob + }, + { + subject: alice, + createdAt: new Date().toISOString(), + }, + sc.getHeaders(bob), + ) + + try { + await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[alice].handle }, + { headers: await network.serviceHeaders(bob) }, + ) + } catch (e) { + expect(e).toBeInstanceOf(BlockedActorError) + } + + // unblock + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: bob, rkey: new AtUri(bobBlockAlice.uri).rkey }, + sc.getHeaders(bob), + ) + }) + + it('liked post(s) author(s) blocks viewer', async () => { + const aliceBlockDan = await pdsAgent.api.app.bsky.graph.block.create( + { + repo: alice, // alice blocks dan + }, + { + subject: dan, + createdAt: new Date().toISOString(), + }, + sc.getHeaders(alice), + ) + + const { data } = await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[bob].handle }, // bob has liked alice's posts + { headers: await network.serviceHeaders(dan) }, + ) + + expect( + data.feed.every((item) => { + return item.post.author.did !== alice + }), + ).toBe(true) // alice's posts are filtered out + + // unblock + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: alice, rkey: new AtUri(aliceBlockDan.uri).rkey }, + sc.getHeaders(alice), + ) + }) + + it('liked post(s) author(s) muted by viewer', async () => { + await pdsAgent.api.app.bsky.graph.muteActor( + { actor: alice }, // dan mutes alice + { headers: sc.getHeaders(dan), encoding: 'application/json' }, + ) + + const { data } = await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[bob].handle }, // bob has liked alice's posts + { headers: await network.serviceHeaders(dan) }, + ) + + expect( + data.feed.every((item) => { + return item.post.author.did !== alice + }), + ).toBe(true) // alice's posts are filtered out + + await pdsAgent.api.app.bsky.graph.unmuteActor( + { actor: alice }, // dan unmutes alice + { headers: sc.getHeaders(dan), encoding: 'application/json' }, + ) + }) }) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts index 449a5732e6e..56204deb1ad 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts @@ -60,14 +60,11 @@ export default function (server: Server, ctx: AppContext) { await assertNoBlocks(ctx, { requester, actor }) feedItemsQb = feedItemsQb .where((qb) => - // hide reposts of muted content - qb - .where('type', '=', 'post') - .orWhere((qb) => - accountService.whereNotMuted(qb, requester, [ - ref('post.creator'), - ]), - ), + qb.where((qb) => + accountService.whereNotMuted(qb, requester, [ + ref('post.creator'), + ]), + ), ) .whereNotExists( graphService.blockQb(requester, [ref('post.creator')]), diff --git a/packages/pds/tests/views/actor-likes.test.ts b/packages/pds/tests/views/actor-likes.test.ts index aec63063d0f..cd0db5e4518 100644 --- a/packages/pds/tests/views/actor-likes.test.ts +++ b/packages/pds/tests/views/actor-likes.test.ts @@ -1,7 +1,11 @@ -import AtpAgent from '@atproto/api' +import AtpAgent, { AtUri } from '@atproto/api' import { runTestServer, CloseFn } from '../_util' import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' +import { + BlockedByActorError, + BlockedActorError, +} from '@atproto/api/src/client/types/app/bsky/feed/getActorLikes' describe('pds actor likes feed views', () => { let agent: AtpAgent @@ -70,4 +74,113 @@ describe('pds actor likes feed views', () => { expect(danLikes).toHaveLength(1) }) + + it('actor blocks viewer', async () => { + const aliceBlockBob = await agent.api.app.bsky.graph.block.create( + { + repo: alice, // alice blocks bob + }, + { + subject: bob, + createdAt: new Date().toISOString(), + }, + sc.getHeaders(alice), + ) + + try { + await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[alice].handle }, + { headers: sc.getHeaders(bob) }, + ) + } catch (e) { + expect(e).toBeInstanceOf(BlockedByActorError) + } + + // unblock + await agent.api.app.bsky.graph.block.delete( + { repo: alice, rkey: new AtUri(aliceBlockBob.uri).rkey }, + sc.getHeaders(alice), + ) + }) + + it('viewer has blocked actor', async () => { + const bobBlockAlice = await agent.api.app.bsky.graph.block.create( + { + repo: bob, // alice blocks bob + }, + { + subject: alice, + createdAt: new Date().toISOString(), + }, + sc.getHeaders(bob), + ) + + try { + await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[alice].handle }, + { headers: sc.getHeaders(bob) }, + ) + } catch (e) { + expect(e).toBeInstanceOf(BlockedActorError) + } + + // unblock + await agent.api.app.bsky.graph.block.delete( + { repo: bob, rkey: new AtUri(bobBlockAlice.uri).rkey }, + sc.getHeaders(bob), + ) + }) + + it('liked post(s) author(s) blocks viewer', async () => { + const aliceBlockDan = await agent.api.app.bsky.graph.block.create( + { + repo: alice, // alice blocks dan + }, + { + subject: dan, + createdAt: new Date().toISOString(), + }, + sc.getHeaders(alice), + ) + + const { data } = await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[bob].handle }, // bob has liked alice's posts + { headers: sc.getHeaders(dan) }, + ) + + expect( + data.feed.every((item) => { + return item.post.author.did !== alice + }), + ).toBe(true) // alice's posts are filtered out + + // unblock + await agent.api.app.bsky.graph.block.delete( + { repo: alice, rkey: new AtUri(aliceBlockDan.uri).rkey }, + sc.getHeaders(alice), + ) + }) + + it('liked post(s) author(s) muted by viewer', async () => { + await agent.api.app.bsky.graph.muteActor( + { actor: alice }, // dan mutes alice + { headers: sc.getHeaders(dan), encoding: 'application/json' }, + ) + + const { data } = await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[bob].handle }, // bob has liked alice's posts + { headers: sc.getHeaders(dan) }, + ) + + expect( + data.feed.every((item) => { + return item.post.author.did !== alice + }), + ).toBe(true) // alice's posts are filtered out + + await agent.api.app.bsky.graph.unmuteActor( + { actor: alice }, // dan unmutes alice + { headers: sc.getHeaders(dan), encoding: 'application/json' }, + ) + }) }) From 1a67abebe4849a8e5a884281ab7f42243f808da9 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 15 Aug 2023 13:58:30 -0500 Subject: [PATCH 140/237] rename file --- .../app/bsky/feed/{getAuthorLikes.json => getActorLikes.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lexicons/app/bsky/feed/{getAuthorLikes.json => getActorLikes.json} (100%) diff --git a/lexicons/app/bsky/feed/getAuthorLikes.json b/lexicons/app/bsky/feed/getActorLikes.json similarity index 100% rename from lexicons/app/bsky/feed/getAuthorLikes.json rename to lexicons/app/bsky/feed/getActorLikes.json From 23d6feb3c9a4e7cde396ec2b68870ee552e52399 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 15 Aug 2023 13:58:40 -0500 Subject: [PATCH 141/237] add method to bsky agent --- packages/api/src/bsky-agent.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 4d623c08bae..95f9dc82715 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -17,6 +17,9 @@ export class BskyAgent extends AtpAgent { getAuthorFeed: typeof this.api.app.bsky.feed.getAuthorFeed = (params, opts) => this.api.app.bsky.feed.getAuthorFeed(params, opts) + getActorLikes: typeof this.api.app.bsky.feed.getActorLikes = (params, opts) => + this.api.app.bsky.feed.getActorLikes(params, opts) + getPostThread: typeof this.api.app.bsky.feed.getPostThread = (params, opts) => this.api.app.bsky.feed.getPostThread(params, opts) From 7bc701e2e1cecae3816efa59a2fb5707b324c933 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 15 Aug 2023 21:44:53 +0200 Subject: [PATCH 142/237] :sparkles: Allow creating moderation action with a duration (#1431) * :sparkles: Add action duration to action model * :sparkles: Add periodic moderation action reversal * :sparkles: Use actionDurationInHours and remove takedownExpiresAt * :sparkles: Destroy mod action reversal job * :white_check_mark: Add test for automatic moderation action reversal * :broom: Port over actionDuration to bsky package * :recycle: Better naming for action duration and minor fixes * :recycle: Optionally build expiresAt based on createdAt when logging action * :recycle: Adjust constructor params * :sparkles: Copy over migration from pds to bsky package * :rotating_light: Fix indentation * :sparkles: Propagate action reversal from app-view to pds * :sparkles: Revert locally only if pushAgent is not found * build * merge dbcoordinator work into periodic mod actions * fix entrypoint for periodic mod reversals * do not run periodic reversals on sequencer leader * fix duration in action detail on bsky * update env for pushing mod reversals * allow zero-duration actions for testing * remove build --------- Co-authored-by: Devin Ivy --- lexicons/com/atproto/admin/defs.json | 14 ++- .../atproto/admin/takeModerationAction.json | 23 +++- packages/api/src/client/lexicons.ts | 20 ++++ .../client/types/com/atproto/admin/defs.ts | 6 ++ .../com/atproto/admin/takeModerationAction.ts | 2 + packages/bsky/service/api.js | 11 ++ .../atproto/admin/reverseModerationAction.ts | 22 +--- .../com/atproto/admin/takeModerationAction.ts | 2 + packages/bsky/src/config.ts | 10 ++ .../20230810T203349843Z-action-duration.ts | 23 ++++ packages/bsky/src/db/migrations/index.ts | 1 + .../db/periodic-moderation-action-reversal.ts | 101 ++++++++++++++++++ packages/bsky/src/db/tables/moderation.ts | 2 + packages/bsky/src/index.ts | 1 + packages/bsky/src/lexicon/lexicons.ts | 20 ++++ .../lexicon/types/com/atproto/admin/defs.ts | 6 ++ .../com/atproto/admin/takeModerationAction.ts | 2 + .../bsky/src/services/moderation/index.ts | 66 +++++++++++- .../bsky/src/services/moderation/views.ts | 26 +++-- packages/bsky/src/util/date.ts | 14 +++ packages/pds/service/index.js | 19 ++++ .../atproto/admin/reverseModerationAction.ts | 22 +--- .../com/atproto/admin/takeModerationAction.ts | 2 + .../20230810T203412859Z-action-duration.ts | 23 ++++ packages/pds/src/db/migrations/index.ts | 1 + .../db/periodic-moderation-action-reversal.ts | 87 +++++++++++++++ packages/pds/src/db/tables/moderation.ts | 2 + packages/pds/src/index.ts | 1 + packages/pds/src/lexicon/lexicons.ts | 20 ++++ .../lexicon/types/com/atproto/admin/defs.ts | 6 ++ .../com/atproto/admin/takeModerationAction.ts | 2 + packages/pds/src/services/moderation/index.ts | 65 +++++++++++ packages/pds/src/services/moderation/views.ts | 26 +++-- packages/pds/src/util/date.ts | 14 +++ packages/pds/tests/moderation.test.ts | 39 +++++++ 35 files changed, 640 insertions(+), 61 deletions(-) create mode 100644 packages/bsky/src/db/migrations/20230810T203349843Z-action-duration.ts create mode 100644 packages/bsky/src/db/periodic-moderation-action-reversal.ts create mode 100644 packages/bsky/src/util/date.ts create mode 100644 packages/pds/src/db/migrations/20230810T203412859Z-action-duration.ts create mode 100644 packages/pds/src/db/periodic-moderation-action-reversal.ts create mode 100644 packages/pds/src/util/date.ts diff --git a/lexicons/com/atproto/admin/defs.json b/lexicons/com/atproto/admin/defs.json index 62235b95213..a04c77d68f8 100644 --- a/lexicons/com/atproto/admin/defs.json +++ b/lexicons/com/atproto/admin/defs.json @@ -17,6 +17,10 @@ "properties": { "id": { "type": "integer" }, "action": { "type": "ref", "ref": "#actionType" }, + "durationInHours": { + "type": "integer", + "description": "Indicates how long this action was meant to be in effect before automatically expiring." + }, "subject": { "type": "union", "refs": ["#repoRef", "com.atproto.repo.strongRef"] @@ -46,6 +50,10 @@ "properties": { "id": { "type": "integer" }, "action": { "type": "ref", "ref": "#actionType" }, + "durationInHours": { + "type": "integer", + "description": "Indicates how long this action was meant to be in effect before automatically expiring." + }, "subject": { "type": "union", "refs": [ @@ -76,7 +84,11 @@ "required": ["id", "action"], "properties": { "id": { "type": "integer" }, - "action": { "type": "ref", "ref": "#actionType" } + "action": { "type": "ref", "ref": "#actionType" }, + "durationInHours": { + "type": "integer", + "description": "Indicates how long this action was meant to be in effect before automatically expiring." + } } }, "actionReversal": { diff --git a/lexicons/com/atproto/admin/takeModerationAction.json b/lexicons/com/atproto/admin/takeModerationAction.json index d9f72c2eb84..76600735251 100644 --- a/lexicons/com/atproto/admin/takeModerationAction.json +++ b/lexicons/com/atproto/admin/takeModerationAction.json @@ -26,11 +26,24 @@ "com.atproto.repo.strongRef" ] }, - "subjectBlobCids": {"type": "array", "items": {"type": "string", "format": "cid"}}, - "createLabelVals": {"type": "array", "items": {"type": "string"}}, - "negateLabelVals": {"type": "array", "items": {"type": "string"}}, - "reason": {"type": "string"}, - "createdBy": {"type": "string", "format": "did"} + "subjectBlobCids": { + "type": "array", + "items": { "type": "string", "format": "cid" } + }, + "createLabelVals": { + "type": "array", + "items": { "type": "string" } + }, + "negateLabelVals": { + "type": "array", + "items": { "type": "string" } + }, + "reason": { "type": "string" }, + "durationInHours": { + "type": "integer", + "description": "Indicates how long this action was meant to be in effect before automatically expiring." + }, + "createdBy": { "type": "string", "format": "did" } } } }, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 956a481627c..67829492559 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -28,6 +28,11 @@ export const schemaDict = { type: 'ref', ref: 'lex:com.atproto.admin.defs#actionType', }, + durationInHours: { + type: 'integer', + description: + 'Indicates how long this action was meant to be in effect before automatically expiring.', + }, subject: { type: 'union', refs: [ @@ -96,6 +101,11 @@ export const schemaDict = { type: 'ref', ref: 'lex:com.atproto.admin.defs#actionType', }, + durationInHours: { + type: 'integer', + description: + 'Indicates how long this action was meant to be in effect before automatically expiring.', + }, subject: { type: 'union', refs: [ @@ -159,6 +169,11 @@ export const schemaDict = { type: 'ref', ref: 'lex:com.atproto.admin.defs#actionType', }, + durationInHours: { + type: 'integer', + description: + 'Indicates how long this action was meant to be in effect before automatically expiring.', + }, }, }, actionReversal: { @@ -1264,6 +1279,11 @@ export const schemaDict = { reason: { type: 'string', }, + durationInHours: { + type: 'integer', + description: + 'Indicates how long this action was meant to be in effect before automatically expiring.', + }, createdBy: { type: 'string', format: 'did', 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 ad2762566d6..f98814ca8e2 100644 --- a/packages/api/src/client/types/com/atproto/admin/defs.ts +++ b/packages/api/src/client/types/com/atproto/admin/defs.ts @@ -13,6 +13,8 @@ import * as ComAtprotoLabelDefs from '../label/defs' export interface ActionView { id: number action: ActionType + /** Indicates how long this action was meant to be in effect before automatically expiring. */ + durationInHours?: number subject: | RepoRef | ComAtprotoRepoStrongRef.Main @@ -43,6 +45,8 @@ export function validateActionView(v: unknown): ValidationResult { export interface ActionViewDetail { id: number action: ActionType + /** Indicates how long this action was meant to be in effect before automatically expiring. */ + durationInHours?: number subject: | RepoView | RepoViewNotFound @@ -75,6 +79,8 @@ export function validateActionViewDetail(v: unknown): ValidationResult { export interface ActionViewCurrent { id: number action: ActionType + /** Indicates how long this action was meant to be in effect before automatically expiring. */ + durationInHours?: number [k: string]: unknown } diff --git a/packages/api/src/client/types/com/atproto/admin/takeModerationAction.ts b/packages/api/src/client/types/com/atproto/admin/takeModerationAction.ts index 4e769609fde..6e253d6b0ef 100644 --- a/packages/api/src/client/types/com/atproto/admin/takeModerationAction.ts +++ b/packages/api/src/client/types/com/atproto/admin/takeModerationAction.ts @@ -25,6 +25,8 @@ export interface InputSchema { createLabelVals?: string[] negateLabelVals?: string[] reason: string + /** Indicates how long this action was meant to be in effect before automatically expiring. */ + durationInHours?: number createdBy: string [k: string]: unknown } diff --git a/packages/bsky/service/api.js b/packages/bsky/service/api.js index ab6b1a8d25e..ec38c55ae55 100644 --- a/packages/bsky/service/api.js +++ b/packages/bsky/service/api.js @@ -21,6 +21,7 @@ const { BskyAppView, ViewMaintainer, makeAlgos, + PeriodicModerationActionReversal, } = require('@atproto/bsky') const main = async () => { @@ -79,9 +80,19 @@ const main = async () => { }) const viewMaintainer = new ViewMaintainer(migrateDb) const viewMaintainerRunning = viewMaintainer.run() + + const periodicModerationActionReversal = new PeriodicModerationActionReversal( + bsky.ctx, + ) + const periodicModerationActionReversalRunning = + periodicModerationActionReversal.run() + await bsky.start() // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/) process.on('SIGTERM', async () => { + // Gracefully shutdown periodic-moderation-action-reversal before destroying bsky instance + periodicModerationActionReversal.destroy() + await periodicModerationActionReversalRunning await bsky.destroy() viewMaintainer.destroy() await viewMaintainerRunning diff --git a/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts b/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts index e62f5371ad9..0e518f44567 100644 --- a/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts +++ b/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts @@ -54,33 +54,13 @@ export default function (server: Server, ctx: AppContext) { ) } - const result = await moderationTxn.logReverseAction({ + const result = await moderationTxn.revertAction({ id, createdAt: now, createdBy, reason, }) - if ( - result.action === TAKEDOWN && - result.subjectType === 'com.atproto.admin.defs#repoRef' && - result.subjectDid - ) { - await moderationTxn.reverseTakedownRepo({ - did: result.subjectDid, - }) - } - - if ( - result.action === TAKEDOWN && - result.subjectType === 'com.atproto.repo.strongRef' && - result.subjectUri - ) { - await moderationTxn.reverseTakedownRecord({ - uri: new AtUri(result.subjectUri), - }) - } - // invert creates & negates const { createLabelVals, negateLabelVals } = result const negate = diff --git a/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts b/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts index 195c0a63a73..f86e0ae0fd1 100644 --- a/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts +++ b/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts @@ -25,6 +25,7 @@ export default function (server: Server, ctx: AppContext) { createLabelVals, negateLabelVals, subjectBlobCids, + durationInHours, } = input.body // apply access rules @@ -63,6 +64,7 @@ export default function (server: Server, ctx: AppContext) { negateLabelVals, createdBy, reason, + durationInHours, }) if ( diff --git a/packages/bsky/src/config.ts b/packages/bsky/src/config.ts index e1ecf89ded0..fbd2fa1b77c 100644 --- a/packages/bsky/src/config.ts +++ b/packages/bsky/src/config.ts @@ -21,6 +21,7 @@ export interface ServerConfigValues { adminPassword: string moderatorPassword?: string triagePassword?: string + moderationActionReverseUrl?: string } export class ServerConfig { @@ -71,6 +72,10 @@ export class ServerConfig { const moderatorPassword = process.env.MODERATOR_PASSWORD || undefined const triagePassword = process.env.TRIAGE_PASSWORD || undefined const labelerDid = process.env.LABELER_DID || 'did:example:labeler' + const moderationActionReverseUrl = + overrides?.moderationActionReverseUrl || + process.env.MODERATION_PUSH_URL || + undefined return new ServerConfig({ version, debugMode, @@ -91,6 +96,7 @@ export class ServerConfig { adminPassword, moderatorPassword, triagePassword, + moderationActionReverseUrl, ...stripUndefineds(overrides ?? {}), }) } @@ -183,6 +189,10 @@ export class ServerConfig { get triagePassword() { return this.cfg.triagePassword } + + get moderationActionReverseUrl() { + return this.cfg.moderationActionReverseUrl + } } function getTagIdxs(str?: string): number[] { diff --git a/packages/bsky/src/db/migrations/20230810T203349843Z-action-duration.ts b/packages/bsky/src/db/migrations/20230810T203349843Z-action-duration.ts new file mode 100644 index 00000000000..0530d4d74fd --- /dev/null +++ b/packages/bsky/src/db/migrations/20230810T203349843Z-action-duration.ts @@ -0,0 +1,23 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .alterTable('moderation_action') + .addColumn('durationInHours', 'integer') + .execute() + await db.schema + .alterTable('moderation_action') + .addColumn('expiresAt', 'varchar') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema + .alterTable('moderation_action') + .dropColumn('durationInHours') + .execute() + await db.schema + .alterTable('moderation_action') + .dropColumn('expiresAt') + .execute() +} diff --git a/packages/bsky/src/db/migrations/index.ts b/packages/bsky/src/db/migrations/index.ts index 5bf06bd5c81..044ec31f8d7 100644 --- a/packages/bsky/src/db/migrations/index.ts +++ b/packages/bsky/src/db/migrations/index.ts @@ -23,3 +23,4 @@ export * as _20230703T045536691Z from './20230703T045536691Z-feed-and-label-indi export * as _20230720T164800037Z from './20230720T164800037Z-posts-cursor-idx' export * as _20230807T035309811Z from './20230807T035309811Z-feed-item-delete-invite-for-user-idx' export * as _20230808T172902639Z from './20230808T172902639Z-repo-rev' +export * as _20230810T203349843Z from './20230810T203349843Z-action-duration' diff --git a/packages/bsky/src/db/periodic-moderation-action-reversal.ts b/packages/bsky/src/db/periodic-moderation-action-reversal.ts new file mode 100644 index 00000000000..be8e4044477 --- /dev/null +++ b/packages/bsky/src/db/periodic-moderation-action-reversal.ts @@ -0,0 +1,101 @@ +import { wait } from '@atproto/common' +import { Leader } from './leader' +import { dbLogger } from '../logger' +import AppContext from '../context' +import AtpAgent from '@atproto/api' +import { buildBasicAuth } from '../auth' + +export const MODERATION_ACTION_REVERSAL_ID = 1011 + +export class PeriodicModerationActionReversal { + leader = new Leader( + MODERATION_ACTION_REVERSAL_ID, + this.appContext.db.getPrimary(), + ) + destroyed = false + pushAgent?: AtpAgent + + constructor(private appContext: AppContext) { + if (appContext.cfg.moderationActionReverseUrl) { + const url = new URL(appContext.cfg.moderationActionReverseUrl) + this.pushAgent = new AtpAgent({ service: url.origin }) + this.pushAgent.api.setHeader( + 'authorization', + buildBasicAuth(url.username, url.password), + ) + } + } + + async revertAction({ id, createdBy }: { id: number; createdBy: string }) { + return this.appContext.db.getPrimary().transaction(async (dbTxn) => { + const moderationTxn = this.appContext.services.moderation(dbTxn) + const reverseAction = { + id, + createdBy, + createdAt: new Date(), + reason: `[SCHEDULED_REVERSAL] Reverting action as originally scheduled`, + } + if (this.pushAgent) { + await this.pushAgent.com.atproto.admin.reverseModerationAction( + reverseAction, + ) + } else { + await moderationTxn.revertAction(reverseAction) + } + }) + } + + async findAndRevertDueActions() { + const moderationService = this.appContext.services.moderation( + this.appContext.db.getPrimary(), + ) + const actionsDueForReversal = + await moderationService.getActionsDueForReversal() + + // 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(actionsDueForReversal.map(this.revertAction.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/moderation.ts b/packages/bsky/src/db/tables/moderation.ts index 36e888e1294..ef2bd3b5f6c 100644 --- a/packages/bsky/src/db/tables/moderation.ts +++ b/packages/bsky/src/db/tables/moderation.ts @@ -34,6 +34,8 @@ export interface ModerationAction { reversedAt: string | null reversedBy: string | null reversedReason: string | null + durationInHours: number | null + expiresAt: string | null } export interface ModerationActionSubjectBlob { diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 213c34374b7..5603761e247 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -29,6 +29,7 @@ export type { ServerConfigValues } from './config' export type { MountedAlgos } from './feed-gen/types' export { ServerConfig } from './config' export { Database, PrimaryDatabase, DatabaseCoordinator } from './db' +export { PeriodicModerationActionReversal } from './db/periodic-moderation-action-reversal' export { Redis } from './redis' export { ViewMaintainer } from './db/views' export { AppContext } from './context' diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 956a481627c..67829492559 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -28,6 +28,11 @@ export const schemaDict = { type: 'ref', ref: 'lex:com.atproto.admin.defs#actionType', }, + durationInHours: { + type: 'integer', + description: + 'Indicates how long this action was meant to be in effect before automatically expiring.', + }, subject: { type: 'union', refs: [ @@ -96,6 +101,11 @@ export const schemaDict = { type: 'ref', ref: 'lex:com.atproto.admin.defs#actionType', }, + durationInHours: { + type: 'integer', + description: + 'Indicates how long this action was meant to be in effect before automatically expiring.', + }, subject: { type: 'union', refs: [ @@ -159,6 +169,11 @@ export const schemaDict = { type: 'ref', ref: 'lex:com.atproto.admin.defs#actionType', }, + durationInHours: { + type: 'integer', + description: + 'Indicates how long this action was meant to be in effect before automatically expiring.', + }, }, }, actionReversal: { @@ -1264,6 +1279,11 @@ export const schemaDict = { reason: { type: 'string', }, + durationInHours: { + type: 'integer', + description: + 'Indicates how long this action was meant to be in effect before automatically expiring.', + }, createdBy: { type: 'string', format: 'did', 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 32edcc85ce1..968252a4c2c 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/defs.ts @@ -13,6 +13,8 @@ import * as ComAtprotoLabelDefs from '../label/defs' export interface ActionView { id: number action: ActionType + /** Indicates how long this action was meant to be in effect before automatically expiring. */ + durationInHours?: number subject: | RepoRef | ComAtprotoRepoStrongRef.Main @@ -43,6 +45,8 @@ export function validateActionView(v: unknown): ValidationResult { export interface ActionViewDetail { id: number action: ActionType + /** Indicates how long this action was meant to be in effect before automatically expiring. */ + durationInHours?: number subject: | RepoView | RepoViewNotFound @@ -75,6 +79,8 @@ export function validateActionViewDetail(v: unknown): ValidationResult { export interface ActionViewCurrent { id: number action: ActionType + /** Indicates how long this action was meant to be in effect before automatically expiring. */ + durationInHours?: number [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/takeModerationAction.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/takeModerationAction.ts index 737fe446fb7..c2cab3f1928 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/takeModerationAction.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/takeModerationAction.ts @@ -26,6 +26,8 @@ export interface InputSchema { createLabelVals?: string[] negateLabelVals?: string[] reason: string + /** Indicates how long this action was meant to be in effect before automatically expiring. */ + durationInHours?: number createdBy: string [k: string]: unknown } diff --git a/packages/bsky/src/services/moderation/index.ts b/packages/bsky/src/services/moderation/index.ts index 241aa9f5439..3463e86605a 100644 --- a/packages/bsky/src/services/moderation/index.ts +++ b/packages/bsky/src/services/moderation/index.ts @@ -7,6 +7,8 @@ import { ModerationAction, ModerationReport } from '../../db/tables/moderation' import { ModerationViews } from './views' import { ImageUriBuilder } from '../../image/uri' import { ImageInvalidator } from '../../image/invalidator' +import { TAKEDOWN } from '../../lexicon/types/com/atproto/admin/defs' +import { addHoursToDate } from '../../util/date' export class ModerationService { constructor( @@ -241,6 +243,7 @@ export class ModerationService { negateLabelVals?: string[] createdBy: string createdAt?: Date + durationInHours?: number }): Promise { this.db.assertTransaction() const { @@ -249,6 +252,7 @@ export class ModerationService { reason, subject, subjectBlobCids, + durationInHours, createdAt = new Date(), } = info const createLabelVals = @@ -290,7 +294,6 @@ export class ModerationService { 'SubjectHasAction', ) } - const actionResult = await this.db.db .insertInto('moderation_action') .values({ @@ -300,6 +303,11 @@ export class ModerationService { createdBy, createLabelVals, negateLabelVals, + durationInHours, + expiresAt: + durationInHours !== undefined + ? addHoursToDate(durationInHours, createdAt).toISOString() + : undefined, ...subjectInfo, }) .returningAll() @@ -330,6 +338,62 @@ export class ModerationService { return actionResult } + async getActionsDueForReversal(): Promise< + Array<{ id: number; createdBy: string }> + > { + const actionsDueForReversal = await this.db.db + .selectFrom('moderation_action') + .where('durationInHours', 'is not', null) + .where('expiresAt', '<', new Date().toISOString()) + .where('reversedAt', 'is', null) + .select(['id', 'createdBy']) + .execute() + + return actionsDueForReversal + } + + async revertAction({ + id, + createdAt, + createdBy, + reason, + }: { + id: number + createdAt: Date + createdBy: string + reason: string + }) { + this.db.assertTransaction() + const result = await this.logReverseAction({ + id, + createdAt, + createdBy, + reason, + }) + + if ( + result.action === TAKEDOWN && + result.subjectType === 'com.atproto.admin.defs#repoRef' && + result.subjectDid + ) { + await this.reverseTakedownRepo({ + did: result.subjectDid, + }) + } + + if ( + result.action === TAKEDOWN && + result.subjectType === 'com.atproto.repo.strongRef' && + result.subjectUri + ) { + await this.reverseTakedownRecord({ + uri: new AtUri(result.subjectUri), + }) + } + + return result + } + async logReverseAction(info: { id: number reason: string diff --git a/packages/bsky/src/services/moderation/views.ts b/packages/bsky/src/services/moderation/views.ts index 9ce0cfbb44a..6c9effe8684 100644 --- a/packages/bsky/src/services/moderation/views.ts +++ b/packages/bsky/src/services/moderation/views.ts @@ -58,7 +58,7 @@ export class ModerationViews { 'in', results.map((r) => r.did), ) - .select(['id', 'action', 'subjectDid']) + .select(['id', 'action', 'durationInHours', 'subjectDid']) .execute(), ]) @@ -88,7 +88,11 @@ export class ModerationViews { indexedAt: r.indexedAt, moderation: { currentAction: action - ? { id: action.id, action: action.action } + ? { + id: action.id, + action: action.action, + durationInHours: action.durationInHours ?? undefined, + } : undefined, }, } @@ -158,7 +162,7 @@ export class ModerationViews { 'in', results.map((r) => r.uri), ) - .select(['id', 'action', 'subjectUri']) + .select(['id', 'action', 'durationInHours', 'subjectUri']) .execute(), ]) const repos = await this.repo(repoResults) @@ -186,7 +190,11 @@ export class ModerationViews { repo, moderation: { currentAction: action - ? { id: action.id, action: action.action } + ? { + id: action.id, + action: action.action, + durationInHours: action.durationInHours ?? undefined, + } : undefined, }, } @@ -275,6 +283,7 @@ export class ModerationViews { const views = results.map((res) => ({ id: res.id, action: res.action, + durationInHours: res.durationInHours ?? undefined, subject: res.subjectType === 'com.atproto.admin.defs#repoRef' ? { @@ -337,6 +346,7 @@ export class ModerationViews { return { id: action.id, action: action.action, + durationInHours: action.durationInHours, subject, subjectBlobs, createLabelVals: action.createLabelVals, @@ -507,7 +517,7 @@ export class ModerationViews { 'in', blobs.map((blob) => blob.ref.toString()), ) - .select(['id', 'action', 'cid']) + .select(['id', 'action', 'durationInHours', 'cid']) .execute() const actionByCid = actionResults.reduce( (acc, cur) => Object.assign(acc, { [cur.cid]: cur }), @@ -526,7 +536,11 @@ export class ModerationViews { createdAt: unknownTime, moderation: { currentAction: action - ? { id: action.id, action: action.action } + ? { + id: action.id, + action: action.action, + durationInHours: action.durationInHours ?? undefined, + } : undefined, }, } diff --git a/packages/bsky/src/util/date.ts b/packages/bsky/src/util/date.ts new file mode 100644 index 00000000000..af9767a0f7f --- /dev/null +++ b/packages/bsky/src/util/date.ts @@ -0,0 +1,14 @@ +/** + * 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/service/index.js b/packages/pds/service/index.js index 1341b38066a..7b94acc9b91 100644 --- a/packages/pds/service/index.js +++ b/packages/pds/service/index.js @@ -23,6 +23,7 @@ const { PDS, ViewMaintainer, makeAlgos, + PeriodicModerationActionReversal, } = require('@atproto/pds') const { Secp256k1Keypair } = require('@atproto/crypto') @@ -84,12 +85,30 @@ const main = async () => { }) const viewMaintainer = new ViewMaintainer(migrateDb) const viewMaintainerRunning = viewMaintainer.run() + + // If the PDS is configured to proxy moderation, this will be running on appview instead of pds. + // Also don't run this on the sequencer leader, which may not be configured regarding moderation proxying at all. + const periodicModerationActionReversal = + pds.ctx.shouldProxyModeration() || pds.ctx.cfg.sequencerLeaderEnabled + ? null + : new PeriodicModerationActionReversal(pds.ctx) + const periodicModerationActionReversalRunning = + periodicModerationActionReversal?.run() + await pds.start() // Graceful shutdown (see also https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/) process.on('SIGTERM', async () => { + // Gracefully shutdown periodic-moderation-action-reversal before destroying pds instance + periodicModerationActionReversal?.destroy() + await periodicModerationActionReversalRunning + await pds.destroy() + + // Gracefully shutdown view-maintainer viewMaintainer.destroy() await viewMaintainerRunning + + // Gracefully shutdown db await migrateDb.close() }) } diff --git a/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts b/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts index c891424b1c2..e5f6bf7835f 100644 --- a/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts @@ -107,33 +107,13 @@ export default function (server: Server, ctx: AppContext) { ) } - const result = await moderationTxn.logReverseAction({ + const result = await moderationTxn.revertAction({ id, createdAt: now, createdBy, reason, }) - if ( - result.action === TAKEDOWN && - result.subjectType === 'com.atproto.admin.defs#repoRef' && - result.subjectDid - ) { - await moderationTxn.reverseTakedownRepo({ - did: result.subjectDid, - }) - } - - if ( - result.action === TAKEDOWN && - result.subjectType === 'com.atproto.repo.strongRef' && - result.subjectUri - ) { - await moderationTxn.reverseTakedownRecord({ - uri: new AtUri(result.subjectUri), - }) - } - // invert creates & negates const { createLabelVals, negateLabelVals } = result const negate = diff --git a/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts b/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts index e6f1f76b737..9e9cea4ce5c 100644 --- a/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts @@ -83,6 +83,7 @@ export default function (server: Server, ctx: AppContext) { createLabelVals, negateLabelVals, subjectBlobCids, + durationInHours, } = input.body // apply access rules @@ -122,6 +123,7 @@ export default function (server: Server, ctx: AppContext) { negateLabelVals, createdBy, reason, + durationInHours, }) if ( diff --git a/packages/pds/src/db/migrations/20230810T203412859Z-action-duration.ts b/packages/pds/src/db/migrations/20230810T203412859Z-action-duration.ts new file mode 100644 index 00000000000..0530d4d74fd --- /dev/null +++ b/packages/pds/src/db/migrations/20230810T203412859Z-action-duration.ts @@ -0,0 +1,23 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .alterTable('moderation_action') + .addColumn('durationInHours', 'integer') + .execute() + await db.schema + .alterTable('moderation_action') + .addColumn('expiresAt', 'varchar') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema + .alterTable('moderation_action') + .dropColumn('durationInHours') + .execute() + await db.schema + .alterTable('moderation_action') + .dropColumn('expiresAt') + .execute() +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index bab1041e5cf..96f74e2d82c 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -60,3 +60,4 @@ export * as _20230801T141349990Z from './20230801T141349990Z-invite-note' export * as _20230801T195109532Z from './20230801T195109532Z-remove-moderation-fkeys' export * as _20230807T035309811Z from './20230807T035309811Z-feed-item-delete-invite-for-user-idx' export * as _20230808T172813122Z from './20230808T172813122Z-repo-rev' +export * as _20230810T203412859Z from './20230810T203412859Z-action-duration' diff --git a/packages/pds/src/db/periodic-moderation-action-reversal.ts b/packages/pds/src/db/periodic-moderation-action-reversal.ts new file mode 100644 index 00000000000..808fba5f369 --- /dev/null +++ b/packages/pds/src/db/periodic-moderation-action-reversal.ts @@ -0,0 +1,87 @@ +import assert from 'assert' +import { wait } from '@atproto/common' +import { Leader } from './leader' +import { dbLogger } from '../logger' +import AppContext from '../context' + +export const MODERATION_ACTION_REVERSAL_ID = 1011 + +export class PeriodicModerationActionReversal { + leader = new Leader(MODERATION_ACTION_REVERSAL_ID, this.appContext.db) + destroyed = false + + constructor(private appContext: AppContext) {} + + async revertAction({ id, createdBy }: { id: number; createdBy: string }) { + return this.appContext.db.transaction(async (dbTxn) => { + const moderationTxn = this.appContext.services.moderation(dbTxn) + await moderationTxn.revertAction({ + id, + createdBy, + createdAt: new Date(), + reason: `[SCHEDULED_REVERSAL] Reverting action as originally scheduled`, + }) + }) + } + + async findAndRevertDueActions() { + const moderationService = this.appContext.services.moderation( + this.appContext.db, + ) + const actionsDueForReversal = + await moderationService.getActionsDueForReversal() + + // 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.allSettled( + actionsDueForReversal.map(this.revertAction.bind(this)), + ) + } + + async run() { + assert( + this.appContext.db.dialect === 'pg', + 'Moderation action reversal can only be run by postgres', + ) + + 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/pds/src/db/tables/moderation.ts b/packages/pds/src/db/tables/moderation.ts index 532661b1fd3..061b3981634 100644 --- a/packages/pds/src/db/tables/moderation.ts +++ b/packages/pds/src/db/tables/moderation.ts @@ -34,6 +34,8 @@ export interface ModerationAction { reversedAt: string | null reversedBy: string | null reversedReason: string | null + durationInHours: number | null + expiresAt: string | null } export interface ModerationActionSubjectBlob { diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 011949b67c5..45f9c8277d6 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -52,6 +52,7 @@ export type { ServerConfigValues } from './config' export { ServerConfig } from './config' export { Database } from './db' export { ViewMaintainer } from './db/views' +export { PeriodicModerationActionReversal } from './db/periodic-moderation-action-reversal' export { DiskBlobStore, MemoryBlobStore } from './storage' export { AppContext } from './context' export { makeAlgos } from './feed-gen' diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 956a481627c..67829492559 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -28,6 +28,11 @@ export const schemaDict = { type: 'ref', ref: 'lex:com.atproto.admin.defs#actionType', }, + durationInHours: { + type: 'integer', + description: + 'Indicates how long this action was meant to be in effect before automatically expiring.', + }, subject: { type: 'union', refs: [ @@ -96,6 +101,11 @@ export const schemaDict = { type: 'ref', ref: 'lex:com.atproto.admin.defs#actionType', }, + durationInHours: { + type: 'integer', + description: + 'Indicates how long this action was meant to be in effect before automatically expiring.', + }, subject: { type: 'union', refs: [ @@ -159,6 +169,11 @@ export const schemaDict = { type: 'ref', ref: 'lex:com.atproto.admin.defs#actionType', }, + durationInHours: { + type: 'integer', + description: + 'Indicates how long this action was meant to be in effect before automatically expiring.', + }, }, }, actionReversal: { @@ -1264,6 +1279,11 @@ export const schemaDict = { reason: { type: 'string', }, + durationInHours: { + type: 'integer', + description: + 'Indicates how long this action was meant to be in effect before automatically expiring.', + }, createdBy: { type: 'string', format: 'did', 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 32edcc85ce1..968252a4c2c 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/defs.ts @@ -13,6 +13,8 @@ import * as ComAtprotoLabelDefs from '../label/defs' export interface ActionView { id: number action: ActionType + /** Indicates how long this action was meant to be in effect before automatically expiring. */ + durationInHours?: number subject: | RepoRef | ComAtprotoRepoStrongRef.Main @@ -43,6 +45,8 @@ export function validateActionView(v: unknown): ValidationResult { export interface ActionViewDetail { id: number action: ActionType + /** Indicates how long this action was meant to be in effect before automatically expiring. */ + durationInHours?: number subject: | RepoView | RepoViewNotFound @@ -75,6 +79,8 @@ export function validateActionViewDetail(v: unknown): ValidationResult { export interface ActionViewCurrent { id: number action: ActionType + /** Indicates how long this action was meant to be in effect before automatically expiring. */ + durationInHours?: number [k: string]: unknown } diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/takeModerationAction.ts b/packages/pds/src/lexicon/types/com/atproto/admin/takeModerationAction.ts index 737fe446fb7..c2cab3f1928 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/takeModerationAction.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/takeModerationAction.ts @@ -26,6 +26,8 @@ export interface InputSchema { createLabelVals?: string[] negateLabelVals?: string[] reason: string + /** Indicates how long this action was meant to be in effect before automatically expiring. */ + durationInHours?: number createdBy: string [k: string]: unknown } diff --git a/packages/pds/src/services/moderation/index.ts b/packages/pds/src/services/moderation/index.ts index 97fa178d878..02433ea4f35 100644 --- a/packages/pds/src/services/moderation/index.ts +++ b/packages/pds/src/services/moderation/index.ts @@ -11,6 +11,8 @@ import { ModerationViews } from './views' import SqlRepoStorage from '../../sql-repo-storage' import { ImageInvalidator } from '../../image/invalidator' import { ImageUriBuilder } from '../../image/uri' +import { TAKEDOWN } from '../../lexicon/types/com/atproto/admin/defs' +import { addHoursToDate } from '../../util/date' export class ModerationService { constructor( @@ -261,6 +263,7 @@ export class ModerationService { negateLabelVals?: string[] createdBy: string createdAt?: Date + durationInHours?: number }): Promise { this.db.assertTransaction() const { @@ -269,6 +272,7 @@ export class ModerationService { reason, subject, subjectBlobCids, + durationInHours, createdAt = new Date(), } = info const createLabelVals = @@ -335,6 +339,11 @@ export class ModerationService { createdBy, createLabelVals, negateLabelVals, + durationInHours, + expiresAt: + durationInHours !== undefined + ? addHoursToDate(durationInHours, createdAt).toISOString() + : undefined, ...subjectInfo, }) .returningAll() @@ -366,6 +375,62 @@ export class ModerationService { return actionResult } + async getActionsDueForReversal(): Promise< + Array<{ id: number; createdBy: string }> + > { + const actionsDueForReversal = await this.db.db + .selectFrom('moderation_action') + // Get entries that have an durationInHours that has passed and have not been reversed + .where('durationInHours', 'is not', null) + .where('expiresAt', '<', new Date().toISOString()) + .where('reversedAt', 'is', null) + .select(['id', 'createdBy']) + .execute() + + return actionsDueForReversal + } + + async revertAction({ + id, + createdAt, + createdBy, + reason, + }: { + id: number + createdAt: Date + createdBy: string + reason: string + }) { + const result = await this.logReverseAction({ + id, + createdAt, + createdBy, + reason, + }) + + if ( + result.action === TAKEDOWN && + result.subjectType === 'com.atproto.admin.defs#repoRef' && + result.subjectDid + ) { + await this.reverseTakedownRepo({ + did: result.subjectDid, + }) + } + + if ( + result.action === TAKEDOWN && + result.subjectType === 'com.atproto.repo.strongRef' && + result.subjectUri + ) { + await this.reverseTakedownRecord({ + uri: new AtUri(result.subjectUri), + }) + } + + return result + } + async logReverseAction(info: { id: number reason: string diff --git a/packages/pds/src/services/moderation/views.ts b/packages/pds/src/services/moderation/views.ts index e0855cb3445..2a0a80297d3 100644 --- a/packages/pds/src/services/moderation/views.ts +++ b/packages/pds/src/services/moderation/views.ts @@ -72,7 +72,7 @@ export class ModerationViews { 'in', results.map((r) => r.did), ) - .select(['id', 'action', 'subjectDid']) + .select(['id', 'action', 'durationInHours', 'subjectDid']) .execute(), this.services .account(this.db) @@ -104,7 +104,11 @@ export class ModerationViews { indexedAt: r.indexedAt, moderation: { currentAction: action - ? { id: action.id, action: action.action } + ? { + id: action.id, + action: action.action, + durationInHours: action.durationInHours ?? undefined, + } : undefined, }, invitedBy: invitedBy[r.did], @@ -194,7 +198,7 @@ export class ModerationViews { 'in', results.map((r) => r.uri), ) - .select(['id', 'action', 'subjectUri']) + .select(['id', 'action', 'durationInHours', 'subjectUri']) .execute(), ]) const repos = await this.repo(repoResults, opts) @@ -226,7 +230,11 @@ export class ModerationViews { repo, moderation: { currentAction: action - ? { id: action.id, action: action.action } + ? { + id: action.id, + action: action.action, + durationInHours: action.durationInHours ?? undefined, + } : undefined, }, } @@ -323,6 +331,7 @@ export class ModerationViews { const views = results.map((res) => ({ id: res.id, action: res.action, + durationInHours: res.durationInHours ?? undefined, subject: res.subjectType === 'com.atproto.admin.defs#repoRef' ? { @@ -383,6 +392,7 @@ export class ModerationViews { return { id: action.id, action: action.action, + durationInHours: action.durationInHours, subject, subjectBlobs, createLabelVals: action.createLabelVals, @@ -556,7 +566,7 @@ export class ModerationViews { 'subject_blob.actionId', 'moderation_action.id', ) - .select(['id', 'action', 'cid']) + .select(['id', 'action', 'durationInHours', 'cid']) .execute(), ]) const actionByCid = actionResults.reduce( @@ -583,7 +593,11 @@ export class ModerationViews { : undefined, moderation: { currentAction: action - ? { id: action.id, action: action.action } + ? { + id: action.id, + action: action.action, + durationInHours: action.durationInHours ?? undefined, + } : undefined, }, } diff --git a/packages/pds/src/util/date.ts b/packages/pds/src/util/date.ts new file mode 100644 index 00000000000..af9767a0f7f --- /dev/null +++ b/packages/pds/src/util/date.ts @@ -0,0 +1,14 @@ +/** + * 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/moderation.test.ts b/packages/pds/tests/moderation.test.ts index 48743809815..fd3731a8668 100644 --- a/packages/pds/tests/moderation.test.ts +++ b/packages/pds/tests/moderation.test.ts @@ -22,6 +22,7 @@ import { REASONOTHER, REASONSPAM, } from '../src/lexicon/types/com/atproto/moderation/defs' +import { PeriodicModerationActionReversal } from '../src' describe('moderation', () => { let server: TestServerInfo @@ -1005,6 +1006,44 @@ describe('moderation', () => { await reverse(action.id) }) + it('automatically reverses actions marked with duration', async () => { + const { data: action } = + await agent.api.com.atproto.admin.takeModerationAction( + { + action: TAKEDOWN, + createdBy: 'did:example:moderator', + reason: 'Y', + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: 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, + }, + { + encoding: 'application/json', + headers: { authorization: moderatorAuth() }, + }, + ) + // In the actual app, this will be instantiated and run on server startup + const periodicReversal = new PeriodicModerationActionReversal(server.ctx) + await periodicReversal.findAndRevertDueActions() + + const { data: reversedAction } = + await agent.api.com.atproto.admin.getModerationAction( + { id: action.id }, + { headers: { authorization: adminAuth() } }, + ) + + // 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(reversedAction.reversal).toMatchObject({ + createdBy: action.createdBy, + reason: '[SCHEDULED_REVERSAL] Reverting action as originally scheduled', + }) + }) + it('does not allow non-full moderators to takedown.', async () => { const attemptTakedownTriage = agent.api.com.atproto.admin.takeModerationAction( From ea9d96e3a44119ca6189e7a3989a1bd9b54989a9 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Tue, 15 Aug 2023 17:12:30 -0400 Subject: [PATCH 143/237] v0.6.4 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index f100155dbdb..16d3c5a9aa5 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.3", + "version": "0.6.4", "main": "src/index.ts", "scripts": { "codegen": "yarn docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From 8eb930382cd5006e79d4f3be7a1f976bcae5c609 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Wed, 16 Aug 2023 14:02:42 -0400 Subject: [PATCH 144/237] Handle db client errors on appview (#1481) handle db client errors on bav --- packages/bsky/src/db/db.ts | 4 ++++ packages/bsky/tests/db.test.ts | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/packages/bsky/src/db/db.ts b/packages/bsky/src/db/db.ts index 7c984892ea1..51787023f20 100644 --- a/packages/bsky/src/db/db.ts +++ b/packages/bsky/src/db/db.ts @@ -3,6 +3,7 @@ import { Kysely, PostgresDialect } from 'kysely' import { Pool as PgPool, types as pgTypes } from 'pg' import DatabaseSchema, { DatabaseSchemaType } from './database-schema' import { PgOptions } from './types' +import { dbLogger } from '../logger' export class Database { pool: PgPool @@ -41,6 +42,7 @@ export class Database { } 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) { @@ -83,3 +85,5 @@ export class Database { } export default Database + +const onClientError = (err: Error) => dbLogger.error({ err }, 'db client error') diff --git a/packages/bsky/tests/db.test.ts b/packages/bsky/tests/db.test.ts index 565f07a0e73..5f92a515586 100644 --- a/packages/bsky/tests/db.test.ts +++ b/packages/bsky/tests/db.test.ts @@ -1,4 +1,5 @@ import { once } from 'events' +import { sql } from 'kysely' import { wait } from '@atproto/common' import { TestNetwork } from '@atproto/dev-env' import { Database } from '../src' @@ -20,6 +21,19 @@ describe('db', () => { 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() + }) + describe('transaction()', () => { it('commits changes', async () => { const result = await db.transaction(async (dbTxn) => { From 34b84131beb21db63dc69bf6f7ba8dc75e172955 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Wed, 16 Aug 2023 18:22:42 -0400 Subject: [PATCH 145/237] Handle db pool errors on appview (#1483) handle db pool errors on bav --- packages/bsky/src/db/db.ts | 2 ++ packages/bsky/tests/db.test.ts | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/packages/bsky/src/db/db.ts b/packages/bsky/src/db/db.ts index 51787023f20..cb58eb4742b 100644 --- a/packages/bsky/src/db/db.ts +++ b/packages/bsky/src/db/db.ts @@ -41,6 +41,7 @@ export class Database { 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 @@ -86,4 +87,5 @@ export class Database { export default Database +const onPoolError = (err: Error) => dbLogger.error({ err }, 'db pool error') const onClientError = (err: Error) => dbLogger.error({ err }, 'db client error') diff --git a/packages/bsky/tests/db.test.ts b/packages/bsky/tests/db.test.ts index 5f92a515586..bb7562e9a92 100644 --- a/packages/bsky/tests/db.test.ts +++ b/packages/bsky/tests/db.test.ts @@ -34,6 +34,17 @@ describe('db', () => { 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) => { From 4241ee16ecef96ddaddfa5489636847a06089c51 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 16 Aug 2023 18:34:19 -0500 Subject: [PATCH 146/237] Handle revalidation (#1474) * easier hanlde revalidation * remove duplicate line * backup handle nameservers on appview * fix tests & add a couple --- packages/bsky/src/config.ts | 11 ++++- packages/bsky/src/index.ts | 6 ++- packages/bsky/src/indexer/config.ts | 9 ++++ packages/bsky/src/indexer/index.ts | 6 ++- packages/bsky/src/services/indexing/index.ts | 22 +++++---- .../com/atproto/admin/updateAccountHandle.ts | 29 +++++++----- .../api/com/atproto/identity/updateHandle.ts | 47 +++++++++++-------- packages/pds/src/services/account/index.ts | 6 +-- packages/pds/tests/handles.test.ts | 7 +++ .../pds/tests/sync/subscribe-repos.test.ts | 18 ++++++- 10 files changed, 114 insertions(+), 47 deletions(-) diff --git a/packages/bsky/src/config.ts b/packages/bsky/src/config.ts index fbd2fa1b77c..2ef7e1edf6c 100644 --- a/packages/bsky/src/config.ts +++ b/packages/bsky/src/config.ts @@ -15,6 +15,7 @@ export interface ServerConfigValues { didPlcUrl: string didCacheStaleTTL: number didCacheMaxTTL: number + handleResolveNameservers?: string[] imgUriEndpoint?: string blobCacheLocation?: string labelerDid: string @@ -45,6 +46,9 @@ export class ServerConfig { process.env.DID_CACHE_MAX_TTL, DAY, ) + const handleResolveNameservers = process.env.HANDLE_RESOLVE_NAMESERVERS + ? process.env.HANDLE_RESOLVE_NAMESERVERS.split(',') + : [] const imgUriEndpoint = process.env.IMG_URI_ENDPOINT const blobCacheLocation = process.env.BLOB_CACHE_LOC const dbPrimaryPostgresUrl = @@ -90,6 +94,7 @@ export class ServerConfig { didPlcUrl, didCacheStaleTTL, didCacheMaxTTL, + handleResolveNameservers, imgUriEndpoint, blobCacheLocation, labelerDid, @@ -159,7 +164,11 @@ export class ServerConfig { } get didCacheMaxTTL() { - return this.cfg.didCacheStaleTTL + return this.cfg.didCacheMaxTTL + } + + get handleResolveNameservers() { + return this.cfg.handleResolveNameservers } get didPlcUrl() { diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 5603761e247..7b00e0d73c6 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -67,7 +67,11 @@ export class BskyAppView { config.didCacheStaleTTL, config.didCacheMaxTTL, ) - const idResolver = new IdResolver({ plcUrl: config.didPlcUrl, didCache }) + const idResolver = new IdResolver({ + plcUrl: config.didPlcUrl, + didCache, + backupNameservers: config.handleResolveNameservers, + }) const imgUriBuilder = new ImageUriBuilder( config.imgUriEndpoint || `${config.publicUrl}/img`, diff --git a/packages/bsky/src/indexer/config.ts b/packages/bsky/src/indexer/config.ts index 24452c67174..d78a0bce93a 100644 --- a/packages/bsky/src/indexer/config.ts +++ b/packages/bsky/src/indexer/config.ts @@ -12,6 +12,7 @@ export interface IndexerConfigValues { didPlcUrl: string didCacheStaleTTL: number didCacheMaxTTL: number + handleResolveNameservers?: string[] labelerDid: string hiveApiKey?: string labelerKeywords: Record @@ -54,6 +55,9 @@ export class IndexerConfig { process.env.DID_CACHE_MAX_TTL, DAY, ) + const handleResolveNameservers = process.env.HANDLE_RESOLVE_NAMESERVERS + ? process.env.HANDLE_RESOLVE_NAMESERVERS.split(',') + : [] const labelerDid = process.env.LABELER_DID || 'did:example:labeler' const labelerPushUrl = overrides?.labelerPushUrl || process.env.LABELER_PUSH_URL || undefined @@ -86,6 +90,7 @@ export class IndexerConfig { didPlcUrl, didCacheStaleTTL, didCacheMaxTTL, + handleResolveNameservers, labelerDid, labelerPushUrl, hiveApiKey, @@ -139,6 +144,10 @@ export class IndexerConfig { return this.cfg.didCacheMaxTTL } + get handleResolveNameservers() { + return this.cfg.handleResolveNameservers + } + get labelerDid() { return this.cfg.labelerDid } diff --git a/packages/bsky/src/indexer/index.ts b/packages/bsky/src/indexer/index.ts index e4c4afed1ab..8bd21120fab 100644 --- a/packages/bsky/src/indexer/index.ts +++ b/packages/bsky/src/indexer/index.ts @@ -36,7 +36,11 @@ export class BskyIndexer { cfg.didCacheStaleTTL, cfg.didCacheMaxTTL, ) - const idResolver = new IdResolver({ plcUrl: cfg.didPlcUrl, didCache }) + const idResolver = new IdResolver({ + plcUrl: cfg.didPlcUrl, + didCache, + backupNameservers: cfg.handleResolveNameservers, + }) const backgroundQueue = new BackgroundQueue(db) let labeler: Labeler if (cfg.hiveApiKey) { diff --git a/packages/bsky/src/services/indexing/index.ts b/packages/bsky/src/services/indexing/index.ts index 120ee396e44..cf30510cbe4 100644 --- a/packages/bsky/src/services/indexing/index.ts +++ b/packages/bsky/src/services/indexing/index.ts @@ -11,7 +11,7 @@ import { } from '@atproto/repo' import { AtUri } from '@atproto/uri' import { IdResolver, getPds } from '@atproto/identity' -import { DAY, chunkArray } from '@atproto/common' +import { DAY, HOUR, chunkArray } from '@atproto/common' import { ValidationError } from '@atproto/lexicon' import { PrimaryDatabase } from '../../db' import * as Post from './plugins/post' @@ -28,6 +28,7 @@ import { subLogger } from '../../logger' import { retryHttp } from '../../util/retry' import { Labeler } from '../../labeler' import { BackgroundQueue } from '../../background' +import { Actor } from '../../db/tables/actor' export class IndexingService { records: { @@ -118,13 +119,7 @@ export class IndexingService { .where('did', '=', did) .selectAll() .executeTakeFirst() - const timestampAt = new Date(timestamp) - const lastIndexedAt = actor && new Date(actor.indexedAt) - const needsReindex = - force || - !lastIndexedAt || - timestampAt.getTime() - lastIndexedAt.getTime() > DAY - if (!needsReindex) { + if (!force && !needsHandleReindex(actor, timestamp)) { return } const atpData = await this.idResolver.did.resolveAtprotoData(did, true) @@ -357,3 +352,14 @@ function* walkContentsWithCids(contents: RepoContentsWithCids) { } } } + +const needsHandleReindex = (actor: Actor | undefined, timestamp: string) => { + if (!actor) return true + const timeDiff = + new Date(timestamp).getTime() - new Date(actor.indexedAt).getTime() + // revalidate daily + if (timeDiff > DAY) return true + // revalidate more aggressively for invalidated handles + if (actor.handle === null && timeDiff > HOUR) return true + return false +} diff --git a/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts b/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts index e394a9f3dd3..44c2ee5d8b1 100644 --- a/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts +++ b/packages/pds/src/api/com/atproto/admin/updateAccountHandle.ts @@ -28,19 +28,24 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Account not found: ${did}`) } - const seqHandleTok = await ctx.db.transaction(async (dbTxn) => { - let tok: HandleSequenceToken - try { - tok = await ctx.services.account(dbTxn).updateHandle(did, handle) - } catch (err) { - if (err instanceof UserAlreadyExistsError) { - throw new InvalidRequestError(`Handle already taken: ${handle}`) + let seqHandleTok: HandleSequenceToken + if (existingAccnt.handle === handle) { + seqHandleTok = { handle, did } + } else { + seqHandleTok = await ctx.db.transaction(async (dbTxn) => { + let tok: HandleSequenceToken + try { + tok = await ctx.services.account(dbTxn).updateHandle(did, handle) + } catch (err) { + if (err instanceof UserAlreadyExistsError) { + throw new InvalidRequestError(`Handle already taken: ${handle}`) + } + throw err } - throw err - } - await ctx.plcClient.updateHandle(did, ctx.plcRotationKey, handle) - return tok - }) + await ctx.plcClient.updateHandle(did, ctx.plcRotationKey, handle) + return tok + }) + } try { await ctx.db.transaction(async (dbTxn) => { diff --git a/packages/pds/src/api/com/atproto/identity/updateHandle.ts b/packages/pds/src/api/com/atproto/identity/updateHandle.ts index 7d07dc807fa..17df7f576cf 100644 --- a/packages/pds/src/api/com/atproto/identity/updateHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/updateHandle.ts @@ -20,28 +20,35 @@ export default function (server: Server, ctx: AppContext) { }) // Pessimistic check to handle spam: also enforced by updateHandle() and the db. - const available = await ctx.services - .account(ctx.db) - .isHandleAvailable(handle) - if (!available) { - throw new InvalidRequestError(`Handle already taken: ${handle}`) - } + const handleDid = await ctx.services.account(ctx.db).getHandleDid(handle) - const seqHandleTok = await ctx.db.transaction(async (dbTxn) => { - let tok: HandleSequenceToken - try { - tok = await ctx.services - .account(dbTxn) - .updateHandle(requester, handle) - } catch (err) { - if (err instanceof UserAlreadyExistsError) { - throw new InvalidRequestError(`Handle already taken: ${handle}`) - } - throw err + let seqHandleTok: HandleSequenceToken + if (handleDid) { + if (handleDid !== requester) { + throw new InvalidRequestError(`Handle already taken: ${handle}`) } - await ctx.plcClient.updateHandle(requester, ctx.plcRotationKey, handle) - return tok - }) + seqHandleTok = { did: requester, handle: handle } + } else { + seqHandleTok = await ctx.db.transaction(async (dbTxn) => { + let tok: HandleSequenceToken + try { + tok = await ctx.services + .account(dbTxn) + .updateHandle(requester, handle) + } catch (err) { + if (err instanceof UserAlreadyExistsError) { + throw new InvalidRequestError(`Handle already taken: ${handle}`) + } + throw err + } + await ctx.plcClient.updateHandle( + requester, + ctx.plcRotationKey, + handle, + ) + return tok + }) + } try { await ctx.db.transaction(async (dbTxn) => { diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index c1f267fd6b0..ecc3f6b9fa1 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -179,14 +179,14 @@ export class AccountService { await sequencer.sequenceEvt(this.db, seqEvt) } - async isHandleAvailable(handle: string) { + async getHandleDid(handle: string): Promise { // @NOTE see also condition in updateHandle() const found = await this.db.db .selectFrom('did_handle') .where('handle', '=', handle) - .select('handle') + .selectAll() .executeTakeFirst() - return !found + return found?.did ?? null } async updateEmail(did: string, email: string) { diff --git a/packages/pds/tests/handles.test.ts b/packages/pds/tests/handles.test.ts index fdcd4a08516..b0827b2e953 100644 --- a/packages/pds/tests/handles.test.ts +++ b/packages/pds/tests/handles.test.ts @@ -130,6 +130,13 @@ describe('handles', () => { await expect(attempt).rejects.toThrow('Handle already taken: bob.test') }) + it('handle updates are idempotent', async () => { + await agent.api.com.atproto.identity.updateHandle( + { handle: 'Bob.test' }, + { headers: sc.getHeaders(bob), encoding: 'application/json' }, + ) + }) + it('if handle update fails, it does not update their did document', async () => { const data = await idResolver.did.resolveAtprotoData(alice) expect(data.handle).toBe(newHandle) diff --git a/packages/pds/tests/sync/subscribe-repos.test.ts b/packages/pds/tests/sync/subscribe-repos.test.ts index 96d948252c4..ad5ab1c14aa 100644 --- a/packages/pds/tests/sync/subscribe-repos.test.ts +++ b/packages/pds/tests/sync/subscribe-repos.test.ts @@ -298,6 +298,7 @@ describe('repo subscribe repos', () => { it('syncs handle changes', async () => { await sc.updateHandle(alice, 'alice2.test') await sc.updateHandle(bob, 'bob2.test') + await sc.updateHandle(bob, 'bob2.test') // idempotent update re-sends const ws = new WebSocket( `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos?cursor=${-1}`, @@ -308,11 +309,26 @@ describe('repo subscribe repos', () => { ws.terminate() await verifyCommitEvents(evts) - const handleEvts = getHandleEvts(evts.slice(-2)) + const handleEvts = getHandleEvts(evts.slice(-3)) verifyHandleEvent(handleEvts[0], alice, 'alice2.test') verifyHandleEvent(handleEvts[1], bob, 'bob2.test') }) + it('resends handle events on idempotent updates', async () => { + const update = sc.updateHandle(bob, 'bob2.test') + + const ws = new WebSocket( + `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos`, + ) + + const gen = byFrame(ws) + const evts = await readTillCaughtUp(gen, update) + ws.terminate() + + const handleEvts = getHandleEvts(evts.slice(-1)) + verifyHandleEvent(handleEvts[0], bob, 'bob2.test') + }) + it('syncs tombstones', async () => { const baddie1 = ( await sc.createAccount('baddie1.test', { From 7c88b43fc5f7b949df2af29bb49f0aa720ac2ed5 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Thu, 17 Aug 2023 15:47:34 +0200 Subject: [PATCH 147/237] =?UTF-8?q?=E2=9C=A8=20Expose=20takendown=20profil?= =?UTF-8?q?e,=20their=20follows=20and=20followers=20to=20mods=20(#1456)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: Allow moderators to see takendown profiles * :sparkles: Allow moderators to see follows and followers of takendown account * :recycle: Let auth check fail on optional verifier * :recycle: Use role type to check moderator access --- .../bsky/src/api/app/bsky/actor/getProfile.ts | 9 +++-- .../src/api/app/bsky/graph/getFollowers.ts | 23 ++++++++---- .../bsky/src/api/app/bsky/graph/getFollows.ts | 23 ++++++++---- packages/bsky/src/auth.ts | 36 +++++++++++++++++++ packages/bsky/src/context.ts | 4 +++ .../app-view/api/app/bsky/actor/getProfile.ts | 16 ++++++--- .../api/app/bsky/graph/getFollowers.ts | 29 +++++++++++---- .../app-view/api/app/bsky/graph/getFollows.ts | 29 +++++++++++---- .../pds/src/app-view/services/actor/views.ts | 34 +++++++++--------- .../pds/src/app-view/services/graph/index.ts | 6 ++-- 10 files changed, 156 insertions(+), 53 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/actor/getProfile.ts b/packages/bsky/src/api/app/bsky/actor/getProfile.ts index 8a7bfbe1abc..aab10478a32 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfile.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfile.ts @@ -6,10 +6,12 @@ import { setRepoRev } from '../../../util' export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.getProfile({ - auth: ctx.authOptionalVerifier, + auth: ctx.authOptionalAccessOrRoleVerifier, handler: async ({ auth, params, res }) => { const { actor } = params - const requester = auth.credentials.did + const requester = 'did' in auth.credentials ? auth.credentials.did : null + const canViewTakendownProfile = + auth.credentials.type === 'role' && auth.credentials.triage const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) @@ -22,7 +24,7 @@ export default function (server: Server, ctx: AppContext) { if (!actorRes) { throw new InvalidRequestError('Profile not found') } - if (softDeleted(actorRes)) { + if (!canViewTakendownProfile && softDeleted(actorRes)) { throw new InvalidRequestError( 'Account has been taken down', 'AccountTakedown', @@ -31,6 +33,7 @@ export default function (server: Server, ctx: AppContext) { const profile = await actorService.views.profileDetailed( actorRes, requester, + { includeSoftDeleted: canViewTakendownProfile }, ) if (!profile) { throw new InvalidRequestError('Profile not found') diff --git a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts index b10b4a0cd64..98f0d336551 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts @@ -6,17 +6,22 @@ import { notSoftDeletedClause } from '../../../../db/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getFollowers({ - auth: ctx.authOptionalVerifier, + auth: ctx.authOptionalAccessOrRoleVerifier, handler: async ({ params, auth }) => { const { actor, limit, cursor } = params - const requester = auth.credentials.did + const requester = 'did' in auth.credentials ? auth.credentials.did : null + const canViewTakendownProfile = + auth.credentials.type === 'role' && auth.credentials.triage const db = ctx.db.getReplica() const { ref } = db.db.dynamic const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) - const subjectRes = await actorService.getActor(actor) + const subjectRes = await actorService.getActor( + actor, + canViewTakendownProfile, + ) if (!subjectRes) { throw new InvalidRequestError(`Actor not found: ${actor}`) } @@ -25,7 +30,9 @@ export default function (server: Server, ctx: AppContext) { .selectFrom('follow') .where('follow.subjectDid', '=', subjectRes.did) .innerJoin('actor as creator', 'creator.did', 'follow.creator') - .where(notSoftDeletedClause(ref('creator'))) + .if(!canViewTakendownProfile, (qb) => + qb.where(notSoftDeletedClause(ref('creator'))), + ) .whereNotExists( graphService.blockQb(requester, [ref('follow.creator')]), ) @@ -47,8 +54,12 @@ export default function (server: Server, ctx: AppContext) { const followersRes = await followersReq.execute() const [followers, subject] = await Promise.all([ - actorService.views.hydrateProfiles(followersRes, requester), - actorService.views.profile(subjectRes, requester), + actorService.views.hydrateProfiles(followersRes, requester, { + includeSoftDeleted: canViewTakendownProfile, + }), + actorService.views.profile(subjectRes, requester, { + includeSoftDeleted: canViewTakendownProfile, + }), ]) if (!subject) { throw new InvalidRequestError(`Actor not found: ${actor}`) diff --git a/packages/bsky/src/api/app/bsky/graph/getFollows.ts b/packages/bsky/src/api/app/bsky/graph/getFollows.ts index e5b576d9804..344b17b0158 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollows.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollows.ts @@ -6,17 +6,22 @@ import { notSoftDeletedClause } from '../../../../db/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getFollows({ - auth: ctx.authOptionalVerifier, + auth: ctx.authOptionalAccessOrRoleVerifier, handler: async ({ params, auth }) => { const { actor, limit, cursor } = params - const requester = auth.credentials.did + const requester = 'did' in auth.credentials ? auth.credentials.did : null + const canViewTakendownProfile = + auth.credentials.type === 'role' && auth.credentials.triage const db = ctx.db.getReplica() const { ref } = db.db.dynamic const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) - const creatorRes = await actorService.getActor(actor) + const creatorRes = await actorService.getActor( + actor, + canViewTakendownProfile, + ) if (!creatorRes) { throw new InvalidRequestError(`Actor not found: ${actor}`) } @@ -25,7 +30,9 @@ export default function (server: Server, ctx: AppContext) { .selectFrom('follow') .where('follow.creator', '=', creatorRes.did) .innerJoin('actor as subject', 'subject.did', 'follow.subjectDid') - .where(notSoftDeletedClause(ref('subject'))) + .if(!canViewTakendownProfile, (qb) => + qb.where(notSoftDeletedClause(ref('subject'))), + ) .whereNotExists( graphService.blockQb(requester, [ref('follow.subjectDid')]), ) @@ -47,8 +54,12 @@ export default function (server: Server, ctx: AppContext) { const followsRes = await followsReq.execute() const [follows, subject] = await Promise.all([ - actorService.views.hydrateProfiles(followsRes, requester), - actorService.views.profile(creatorRes, requester), + actorService.views.hydrateProfiles(followsRes, requester, { + includeSoftDeleted: canViewTakendownProfile, + }), + actorService.views.profile(creatorRes, requester, { + includeSoftDeleted: canViewTakendownProfile, + }), ]) if (!subject) { throw new InvalidRequestError(`Actor not found: ${actor}`) diff --git a/packages/bsky/src/auth.ts b/packages/bsky/src/auth.ts index 7c73e9f1af7..a90cf5fa410 100644 --- a/packages/bsky/src/auth.ts +++ b/packages/bsky/src/auth.ts @@ -30,6 +30,42 @@ export const authOptionalVerifier = return authVerifier(idResolver, opts)(reqCtx) } +export const authOptionalAccessOrRoleVerifier = ( + idResolver: IdResolver, + cfg: ServerConfig, +) => { + const verifyAccess = authVerifier(idResolver, { aud: cfg.serverDid }) + const verifyRole = roleVerifier(cfg) + return async (ctx: { req: express.Request; res: express.Response }) => { + const defaultUnAuthorizedCredentials = { + credentials: { did: null, type: 'unauthed' as const }, + } + if (!ctx.req.headers.authorization) { + return defaultUnAuthorizedCredentials + } + // For non-admin tokens, we don't want to consider alternative verifiers and let it fail if it fails + const isRoleAuthToken = ctx.req.headers.authorization?.startsWith(BASIC) + if (isRoleAuthToken) { + const result = await verifyRole(ctx) + return { + ...result, + credentials: { + type: 'role' as const, + ...result.credentials, + }, + } + } + const result = await verifyAccess(ctx) + return { + ...result, + credentials: { + type: 'access' as const, + ...result.credentials, + }, + } + } +} + export const roleVerifier = (cfg: ServerConfig) => async (reqCtx: { req: express.Request; res: express.Response }) => { diff --git a/packages/bsky/src/context.ts b/packages/bsky/src/context.ts index ed322e411a1..a81aa8eab5a 100644 --- a/packages/bsky/src/context.ts +++ b/packages/bsky/src/context.ts @@ -71,6 +71,10 @@ export class AppContext { }) } + get authOptionalAccessOrRoleVerifier() { + return auth.authOptionalAccessOrRoleVerifier(this.idResolver, this.cfg) + } + get roleVerifier() { return auth.roleVerifier(this.cfg) } diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts b/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts index f771bd684c8..1826b583f54 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts @@ -2,19 +2,23 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../../lexicon' import { softDeleted } from '../../../../../db/util' import AppContext from '../../../../../context' +import { authPassthru } from '../../../../../api/com/atproto/admin/util' import { OutputSchema } from '../../../../../lexicon/types/app/bsky/actor/getProfile' import { handleReadAfterWrite } from '../util/read-after-write' import { LocalRecords } from '../../../../../services/local' export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.getProfile({ - auth: ctx.accessVerifier, + auth: ctx.accessOrRoleVerifier, handler: async ({ req, auth, params }) => { - const requester = auth.credentials.did + const requester = + auth.credentials.type === 'access' ? auth.credentials.did : null if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile( params, - await ctx.serviceAuthHeaders(requester), + requester + ? await ctx.serviceAuthHeaders(requester) + : authPassthru(req), ) if (res.data.did === requester) { return await handleReadAfterWrite( @@ -30,6 +34,9 @@ export default function (server: Server, ctx: AppContext) { } } + // As long as user has triage permission, we know that they are a moderator user and can see taken down profiles + const canViewTakendownProfile = + auth.credentials.type === 'role' && auth.credentials.triage const { actor } = params const { db, services } = ctx const actorService = services.appView.actor(db) @@ -39,7 +46,7 @@ export default function (server: Server, ctx: AppContext) { if (!actorRes) { throw new InvalidRequestError('Profile not found') } - if (softDeleted(actorRes)) { + if (!canViewTakendownProfile && softDeleted(actorRes)) { throw new InvalidRequestError( 'Account has been taken down', 'AccountTakedown', @@ -48,6 +55,7 @@ export default function (server: Server, ctx: AppContext) { const profile = await actorService.views.profileDetailed( actorRes, requester, + { includeSoftDeleted: canViewTakendownProfile }, ) if (!profile) { throw new InvalidRequestError('Profile not found') diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts index 6bec286398a..e7f307c2a24 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts @@ -3,16 +3,20 @@ import { Server } from '../../../../../lexicon' import { paginate, TimeCidKeyset } from '../../../../../db/pagination' import AppContext from '../../../../../context' import { notSoftDeletedClause } from '../../../../../db/util' +import { authPassthru } from '../../../../../api/com/atproto/admin/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getFollowers({ - auth: ctx.accessVerifier, + auth: ctx.accessOrRoleVerifier, handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did + const requester = + auth.credentials.type === 'access' ? auth.credentials.did : null if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.graph.getFollowers( params, - await ctx.serviceAuthHeaders(requester), + requester + ? await ctx.serviceAuthHeaders(requester) + : authPassthru(req), ) return { encoding: 'application/json', @@ -20,6 +24,8 @@ export default function (server: Server, ctx: AppContext) { } } + const canViewTakendownProfile = + auth.credentials.type === 'role' && auth.credentials.triage const { actor, limit, cursor } = params const { services, db } = ctx const { ref } = db.db.dynamic @@ -27,7 +33,10 @@ export default function (server: Server, ctx: AppContext) { const actorService = services.appView.actor(db) const graphService = services.appView.graph(db) - const subjectRes = await actorService.getActor(actor) + const subjectRes = await actorService.getActor( + actor, + canViewTakendownProfile, + ) if (!subjectRes) { throw new InvalidRequestError(`Actor not found: ${actor}`) } @@ -41,7 +50,9 @@ export default function (server: Server, ctx: AppContext) { 'creator_repo.did', 'follow.creator', ) - .where(notSoftDeletedClause(ref('creator_repo'))) + .if(canViewTakendownProfile, (qb) => + qb.where(notSoftDeletedClause(ref('creator_repo'))), + ) .whereNotExists( graphService.blockQb(requester, [ref('follow.creator')]), ) @@ -66,8 +77,12 @@ export default function (server: Server, ctx: AppContext) { const followersRes = await followersReq.execute() const [followers, subject] = await Promise.all([ - actorService.views.hydrateProfiles(followersRes, requester), - actorService.views.profile(subjectRes, requester), + actorService.views.hydrateProfiles(followersRes, requester, { + includeSoftDeleted: canViewTakendownProfile, + }), + actorService.views.profile(subjectRes, requester, { + includeSoftDeleted: canViewTakendownProfile, + }), ]) if (!subject) { throw new InvalidRequestError(`Actor not found: ${actor}`) diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts b/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts index 5677999c4b6..59863835aa9 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts @@ -3,16 +3,20 @@ import { Server } from '../../../../../lexicon' import { paginate, TimeCidKeyset } from '../../../../../db/pagination' import AppContext from '../../../../../context' import { notSoftDeletedClause } from '../../../../../db/util' +import { authPassthru } from '../../../../../api/com/atproto/admin/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getFollows({ - auth: ctx.accessVerifier, + auth: ctx.accessOrRoleVerifier, handler: async ({ req, params, auth }) => { - const requester = auth.credentials.did + const requester = + auth.credentials.type === 'access' ? auth.credentials.did : null if (ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.app.bsky.graph.getFollows( params, - await ctx.serviceAuthHeaders(requester), + requester + ? await ctx.serviceAuthHeaders(requester) + : authPassthru(req), ) return { encoding: 'application/json', @@ -20,6 +24,8 @@ export default function (server: Server, ctx: AppContext) { } } + const canViewTakendownProfile = + auth.credentials.type === 'role' && auth.credentials.triage const { actor, limit, cursor } = params const { services, db } = ctx const { ref } = db.db.dynamic @@ -27,7 +33,10 @@ export default function (server: Server, ctx: AppContext) { const actorService = services.appView.actor(db) const graphService = services.appView.graph(db) - const creatorRes = await actorService.getActor(actor) + const creatorRes = await actorService.getActor( + actor, + canViewTakendownProfile, + ) if (!creatorRes) { throw new InvalidRequestError(`Actor not found: ${actor}`) } @@ -41,7 +50,9 @@ export default function (server: Server, ctx: AppContext) { 'subject_repo.did', 'follow.subjectDid', ) - .where(notSoftDeletedClause(ref('subject_repo'))) + .if(!canViewTakendownProfile, (qb) => + qb.where(notSoftDeletedClause(ref('subject_repo'))), + ) .whereNotExists( graphService.blockQb(requester, [ref('follow.subjectDid')]), ) @@ -66,8 +77,12 @@ export default function (server: Server, ctx: AppContext) { const followsRes = await followsReq.execute() const [follows, subject] = await Promise.all([ - actorService.views.hydrateProfiles(followsRes, requester), - actorService.views.profile(creatorRes, requester), + actorService.views.hydrateProfiles(followsRes, requester, { + includeSoftDeleted: canViewTakendownProfile, + }), + actorService.views.profile(creatorRes, requester, { + includeSoftDeleted: canViewTakendownProfile, + }), ]) if (!subject) { throw new InvalidRequestError(`Actor not found: ${actor}`) diff --git a/packages/pds/src/app-view/services/actor/views.ts b/packages/pds/src/app-view/services/actor/views.ts index 1d5468cedd2..3e4a1a91f7f 100644 --- a/packages/pds/src/app-view/services/actor/views.ts +++ b/packages/pds/src/app-view/services/actor/views.ts @@ -27,7 +27,7 @@ export class ActorViews { async profilesDetailed( results: ActorResult[], - viewer: string, + viewer: string | null, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise> { if (results.length === 0) return {} @@ -67,38 +67,38 @@ export class ActorViews { 'ipld_block.content as profileBytes', this.db.db .selectFrom('follow') - .where('creator', '=', viewer) + .where('creator', '=', viewer ?? '') .whereRef('subjectDid', '=', ref('did_handle.did')) .select('uri') .as('requesterFollowing'), this.db.db .selectFrom('follow') .whereRef('creator', '=', ref('did_handle.did')) - .where('subjectDid', '=', viewer) + .where('subjectDid', '=', viewer ?? '') .select('uri') .as('requesterFollowedBy'), this.db.db .selectFrom('actor_block') - .where('creator', '=', viewer) + .where('creator', '=', viewer ?? '') .whereRef('subjectDid', '=', ref('did_handle.did')) .select('uri') .as('requesterBlocking'), this.db.db .selectFrom('actor_block') .whereRef('creator', '=', ref('did_handle.did')) - .where('subjectDid', '=', viewer) + .where('subjectDid', '=', viewer ?? '') .select('uri') .as('requesterBlockedBy'), this.db.db .selectFrom('mute') .whereRef('did', '=', ref('did_handle.did')) - .where('mutedByDid', '=', viewer) + .where('mutedByDid', '=', viewer ?? '') .select('did') .as('requesterMuted'), this.db.db .selectFrom('list_item') .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') - .where('list_mute.mutedByDid', '=', viewer) + .where('list_mute.mutedByDid', '=', viewer ?? '') .whereRef('list_item.subjectDid', '=', ref('did_handle.did')) .select('list_item.listUri') .limit(1) @@ -171,7 +171,7 @@ export class ActorViews { async profileDetailed( result: ActorResult, - viewer: string, + viewer: string | null, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise { const profiles = await this.profilesDetailed([result], viewer, opts) @@ -180,7 +180,7 @@ export class ActorViews { async profiles( results: ActorResult[], - viewer: string, + viewer: string | null, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise> { if (results.length === 0) return {} @@ -214,38 +214,38 @@ export class ActorViews { 'ipld_block.content as profileBytes', this.db.db .selectFrom('follow') - .where('creator', '=', viewer) + .where('creator', '=', viewer ?? '') .whereRef('subjectDid', '=', ref('did_handle.did')) .select('uri') .as('requesterFollowing'), this.db.db .selectFrom('follow') .whereRef('creator', '=', ref('did_handle.did')) - .where('subjectDid', '=', viewer) + .where('subjectDid', '=', viewer ?? '') .select('uri') .as('requesterFollowedBy'), this.db.db .selectFrom('actor_block') - .where('creator', '=', viewer) + .where('creator', '=', viewer ?? '') .whereRef('subjectDid', '=', ref('did_handle.did')) .select('uri') .as('requesterBlocking'), this.db.db .selectFrom('actor_block') .whereRef('creator', '=', ref('did_handle.did')) - .where('subjectDid', '=', viewer) + .where('subjectDid', '=', viewer ?? '') .select('uri') .as('requesterBlockedBy'), this.db.db .selectFrom('mute') .whereRef('did', '=', ref('did_handle.did')) - .where('mutedByDid', '=', viewer) + .where('mutedByDid', '=', viewer ?? '') .select('did') .as('requesterMuted'), this.db.db .selectFrom('list_item') .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') - .where('list_mute.mutedByDid', '=', viewer) + .where('list_mute.mutedByDid', '=', viewer ?? '') .whereRef('list_item.subjectDid', '=', ref('did_handle.did')) .select('list_item.listUri') .limit(1) @@ -302,7 +302,7 @@ export class ActorViews { async hydrateProfiles( results: ActorResult[], - viewer: string, + viewer: string | null, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise { const profiles = await this.profiles(results, viewer, opts) @@ -311,7 +311,7 @@ export class ActorViews { async profile( result: ActorResult, - viewer: string, + viewer: string | null, opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, ): Promise { const profiles = await this.profiles([result], viewer, opts) diff --git a/packages/pds/src/app-view/services/graph/index.ts b/packages/pds/src/app-view/services/graph/index.ts index 1a3cee80da6..6df2d0f10d4 100644 --- a/packages/pds/src/app-view/services/graph/index.ts +++ b/packages/pds/src/app-view/services/graph/index.ts @@ -38,7 +38,7 @@ export class GraphService { .select(['list_item.cid as cid', 'list_item.createdAt as createdAt']) } - blockQb(requester: string, refs: NotEmptyArray) { + blockQb(requester: string | null, refs: NotEmptyArray) { const subjectRefs = sql.join(refs) return this.db.db .selectFrom('actor_block') @@ -46,12 +46,12 @@ export class GraphService { outer .where((qb) => qb - .where('actor_block.creator', '=', requester) + .where('actor_block.creator', '=', requester ?? '') .whereRef('actor_block.subjectDid', 'in', sql`(${subjectRefs})`), ) .orWhere((qb) => qb - .where('actor_block.subjectDid', '=', requester) + .where('actor_block.subjectDid', '=', requester ?? '') .whereRef('actor_block.creator', 'in', sql`(${subjectRefs})`), ), ) From cd0cf159493d242a2f5808407415021d48016181 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Thu, 17 Aug 2023 11:08:03 -0400 Subject: [PATCH 148/237] Fix condition for viewing soft-deleted followers (#1485) fix condition for viewing soft-deleted followers --- packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts index e7f307c2a24..1e0c0779cce 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts @@ -50,7 +50,7 @@ export default function (server: Server, ctx: AppContext) { 'creator_repo.did', 'follow.creator', ) - .if(canViewTakendownProfile, (qb) => + .if(!canViewTakendownProfile, (qb) => qb.where(notSoftDeletedClause(ref('creator_repo'))), ) .whereNotExists( From 1a5b4227189814ce141c4af26d8263819aaf0fd1 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 17 Aug 2023 12:10:36 -0500 Subject: [PATCH 149/237] only include media posts by post creator --- .../src/api/app/bsky/feed/getAuthorFeed.ts | 17 +++-- packages/bsky/tests/views/author-feed.test.ts | 55 ++++++-------- .../api/app/bsky/feed/getAuthorFeed.ts | 51 +++++++------ packages/pds/tests/views/author-feed.test.ts | 74 +++++++++---------- 4 files changed, 99 insertions(+), 98 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index e44f78cc373..078f2982988 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -55,13 +55,16 @@ export default function (server: Server, ctx: AppContext) { .where('originatorDid', '=', did) if (filter === 'posts_with_media') { - // only posts with media - feedItemsQb = feedItemsQb.whereExists((qb) => - qb - .selectFrom('post_embed_image') - .select('post_embed_image.postUri') - .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'), - ) + feedItemsQb = feedItemsQb + // and only your own posts/reposts + .where('post.creator', '=', did) + // only posts with media + .whereExists((qb) => + qb + .selectFrom('post_embed_image') + .select('post_embed_image.postUri') + .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'), + ) } else if (filter === 'posts_no_replies') { // only posts, no replies feedItemsQb = feedItemsQb.where('post.replyParent', 'is', null) diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index 515a826dcde..590e9417f79 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -236,51 +236,44 @@ describe('pds author feed views', () => { ) }) - it('can filter by type', async () => { - const { data: allFeed } = await agent.api.app.bsky.feed.getAuthorFeed({ + it('can filter by posts_with_media', async () => { + const { data: carolFeed } = await agent.api.app.bsky.feed.getAuthorFeed({ actor: carol, + filter: 'posts_with_media', }) + expect(carolFeed.feed.length).toBeGreaterThan(0) expect( - allFeed.feed.some(({ post }) => { - return isRecord(post.record) && Boolean(post.record.reply) - }), - ).toBeTruthy() - expect( - allFeed.feed.some(({ post }) => { - return ( - (isEmbedRecordWithMedia(post.embed) && - isImageEmbed(post.embed?.media)) || - isImageEmbed(post.embed) - ) + carolFeed.feed.every(({ post }) => { + const isRecordWithActorMedia = + isEmbedRecordWithMedia(post.embed) && isImageEmbed(post.embed?.media) + const isActorMedia = isImageEmbed(post.embed) + const isFromActor = post.author.did === carol + + return (isRecordWithActorMedia || isActorMedia) && isFromActor }), ).toBeTruthy() - const { data: mediaFeed } = await agent.api.app.bsky.feed.getAuthorFeed({ - actor: carol, + const { data: bobFeed } = await agent.api.app.bsky.feed.getAuthorFeed({ + actor: bob, filter: 'posts_with_media', }) - const { data: imagesOnlyFeed } = - await agent.api.app.bsky.feed.getAuthorFeed({ - actor: bob, - filter: 'posts_with_media', - }) expect( - mediaFeed.feed.every(({ post }) => { - return ( - (isEmbedRecordWithMedia(post.embed) && - isImageEmbed(post.embed?.media)) || - isImageEmbed(post.embed) - ) - }), - ).toBeTruthy() - expect( - imagesOnlyFeed.feed.every(({ post }) => { - return isImageEmbed(post.embed) + bobFeed.feed.every(({ post }) => { + return isImageEmbed(post.embed) && post.author.did === bob }), ).toBeTruthy() + const { data: danFeed } = await agent.api.app.bsky.feed.getAuthorFeed({ + actor: dan, + filter: 'posts_with_media', + }) + + expect(danFeed.feed.length).toEqual(0) + }) + + it('filters by posts_no_replies', async () => { const { data: postsOnlyFeed } = await agent.api.app.bsky.feed.getAuthorFeed( { actor: carol, filter: 'posts_no_replies' }, ) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index 7c798b7d215..548cf067008 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -1,3 +1,4 @@ +import { sql } from 'kysely' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../../lexicon' import { FeedKeyset } from '../util/feed' @@ -38,17 +39,37 @@ export default function (server: Server, ctx: AppContext) { const feedService = ctx.services.appView.feed(ctx.db) const graphService = ctx.services.appView.graph(ctx.db) + // maybe resolve did first + let actorDid = '' + if (actor.startsWith('did:')) { + actorDid = actor + } else { + const actorRes = await ctx.db.db + .selectFrom('did_handle') + .select('did') + .where('did_handle.handle', '=', actor) + .executeTakeFirst() + if (actorRes) { + actorDid = actorRes?.did + } + } + // defaults to posts, reposts, and replies - let feedItemsQb = getFeedItemsQb(ctx, { actor }) + let feedItemsQb = feedService + .selectFeedItemQb() + .where('originatorDid', '=', actorDid) if (params.filter === 'posts_with_media') { - // only posts with media - feedItemsQb = feedItemsQb.whereExists((qb) => - qb - .selectFrom('post_embed_image') - .select('post_embed_image.postUri') - .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'), - ) + feedItemsQb = feedItemsQb + // and only your own posts/reposts + .where('post.creator', '=', actorDid) + // only posts with media + .whereExists((qb) => + qb + .selectFrom('post_embed_image') + .select('post_embed_image.postUri') + .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'), + ) } else if (params.filter === 'posts_no_replies') { // only posts, no replies feedItemsQb = feedItemsQb.where('post.replyParent', 'is', null) @@ -100,20 +121,6 @@ export default function (server: Server, ctx: AppContext) { }) } -function getFeedItemsQb(ctx: AppContext, opts: { actor: string }) { - const { actor } = opts - const feedService = ctx.services.appView.feed(ctx.db) - const userLookupCol = actor.startsWith('did:') - ? 'did_handle.did' - : 'did_handle.handle' - const actorDidQb = ctx.db.db - .selectFrom('did_handle') - .select('did') - .where(userLookupCol, '=', actor) - .limit(1) - return feedService.selectFeedItemQb().where('originatorDid', '=', actorDidQb) -} - // throws when there's a block between the two users async function assertNoBlocks( ctx: AppContext, diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index 389d1f752d3..30663677514 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -279,62 +279,60 @@ describe('pds author feed views', () => { await reverseModerationAction(takedownAction.id) }) - it('can filter by type', async () => { - const { data: allFeed } = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: carol }, - { headers: sc.getHeaders(alice) }, + it('can filter by posts_with_media', async () => { + const { data: carolFeed } = await agent.api.app.bsky.feed.getAuthorFeed( + { + actor: carol, + filter: 'posts_with_media', + }, + { + headers: sc.getHeaders(alice), + }, ) + expect(carolFeed.feed.length).toBeGreaterThan(0) expect( - allFeed.feed.some(({ post }) => { - return isRecord(post.record) && Boolean(post.record.reply) - }), - ).toBeTruthy() - expect( - allFeed.feed.some(({ post }) => { - return ( - (isEmbedRecordWithMedia(post.embed) && - isImageEmbed(post.embed?.media)) || - isImageEmbed(post.embed) - ) + carolFeed.feed.every(({ post }) => { + const isRecordWithActorMedia = + isEmbedRecordWithMedia(post.embed) && isImageEmbed(post.embed?.media) + const isActorMedia = isImageEmbed(post.embed) + const isFromActor = post.author.did === carol + + return (isRecordWithActorMedia || isActorMedia) && isFromActor }), ).toBeTruthy() - const { data: mediaFeed } = await agent.api.app.bsky.feed.getAuthorFeed( + const { data: bobFeed } = await agent.api.app.bsky.feed.getAuthorFeed( { - actor: carol, + actor: bob, filter: 'posts_with_media', }, { headers: sc.getHeaders(alice), }, ) - const { data: imagesOnlyFeed } = - await agent.api.app.bsky.feed.getAuthorFeed( - { - actor: bob, - filter: 'posts_with_media', - }, - { - headers: sc.getHeaders(alice), - }, - ) + expect(bobFeed.feed.length).toBeGreaterThan(0) expect( - mediaFeed.feed.every(({ post }) => { - return ( - (isEmbedRecordWithMedia(post.embed) && - isImageEmbed(post.embed?.media)) || - isImageEmbed(post.embed) - ) - }), - ).toBeTruthy() - expect( - imagesOnlyFeed.feed.every(({ post }) => { - return isImageEmbed(post.embed) + bobFeed.feed.every(({ post }) => { + return isImageEmbed(post.embed) && post.author.did === bob }), ).toBeTruthy() + const { data: danFeed } = await agent.api.app.bsky.feed.getAuthorFeed( + { + actor: dan, + filter: 'posts_with_media', + }, + { + headers: sc.getHeaders(alice), + }, + ) + + expect(danFeed.feed.length).toEqual(0) + }) + + it('filters by posts_no_replies', async () => { const { data: postsOnlyFeed } = await agent.api.app.bsky.feed.getAuthorFeed( { actor: carol, filter: 'posts_no_replies' }, { headers: sc.getHeaders(alice) }, From ff0d2df6e3b642e94bbd9b539a1a700b99c1d07e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 17 Aug 2023 13:32:19 -0500 Subject: [PATCH 150/237] use getActor, failing atm --- .../src/api/app/bsky/feed/getAuthorFeed.ts | 21 +++++++------------ .../api/app/bsky/feed/getAuthorFeed.ts | 17 +++++---------- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 078f2982988..6bc6bebe046 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -35,29 +35,22 @@ export default function (server: Server, ctx: AppContext) { const feedService = ctx.services.feed(db) const graphService = ctx.services.graph(db) - let did = '' - if (actor.startsWith('did:')) { - did = actor - } else { - const actorRes = await db.db - .selectFrom('actor') - .select('did') - .where('handle', '=', actor) - .executeTakeFirst() - if (actorRes) { - did = actorRes?.did - } + // maybe resolve did first + const actorRes = await actorService.getActor(actor) + if (!actorRes) { + throw new InvalidRequestError('Profile not found') } + const actorDid = actorRes.did // defaults to posts, reposts, and replies let feedItemsQb = feedService .selectFeedItemQb() - .where('originatorDid', '=', did) + .where('originatorDid', '=', actorDid) if (filter === 'posts_with_media') { feedItemsQb = feedItemsQb // and only your own posts/reposts - .where('post.creator', '=', did) + .where('post.creator', '=', actorDid) // only posts with media .whereExists((qb) => qb diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index 548cf067008..2c88f774804 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -36,23 +36,16 @@ export default function (server: Server, ctx: AppContext) { const { ref } = ctx.db.db.dynamic const accountService = ctx.services.account(ctx.db) + const actorService = ctx.services.appView.actor(ctx.db) const feedService = ctx.services.appView.feed(ctx.db) const graphService = ctx.services.appView.graph(ctx.db) // maybe resolve did first - let actorDid = '' - if (actor.startsWith('did:')) { - actorDid = actor - } else { - const actorRes = await ctx.db.db - .selectFrom('did_handle') - .select('did') - .where('did_handle.handle', '=', actor) - .executeTakeFirst() - if (actorRes) { - actorDid = actorRes?.did - } + const actorRes = await actorService.getActor(actor) + if (!actorRes) { + throw new InvalidRequestError('Profile not found') } + const actorDid = actorRes.did // defaults to posts, reposts, and replies let feedItemsQb = feedService From 4347a21d928c55f26750759705534745252cf0a1 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 17 Aug 2023 13:50:26 -0500 Subject: [PATCH 151/237] fix actor takedown tests --- packages/bsky/tests/views/author-feed.test.ts | 14 ++++++++------ packages/pds/tests/views/author-feed.test.ts | 15 +++++++++------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index 590e9417f79..59f5bac5e58 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -165,12 +165,14 @@ describe('pds author feed views', () => { }, ) - const { data: postBlock } = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: alice }, - { headers: await network.serviceHeaders(carol) }, - ) - - expect(postBlock.feed.length).toEqual(0) + try { + const { data: postBlock } = await agent.api.app.bsky.feed.getAuthorFeed( + { actor: alice }, + { headers: await network.serviceHeaders(carol) }, + ) + } catch (e: any) { + expect(e.message).toEqual('Profile not found') + } // Cleanup await agent.api.com.atproto.admin.reverseModerationAction( diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index 30663677514..8e3e7a4071f 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -12,6 +12,7 @@ import basicSeed from '../seeds/basic' import { isRecord } from '../../src/lexicon/types/app/bsky/feed/post' import { isView as isEmbedRecordWithMedia } from '../../src/lexicon/types/app/bsky/embed/recordWithMedia' import { isView as isImageEmbed } from '../../src/lexicon/types/app/bsky/embed/images' +import { InvalidRequestError } from '@atproto/xrpc-server' describe('pds author feed views', () => { let agent: AtpAgent @@ -196,12 +197,14 @@ describe('pds author feed views', () => { did: alice, }) - const { data: postBlock } = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: alice }, - { headers: sc.getHeaders(carol) }, - ) - - expect(postBlock.feed.length).toEqual(0) + try { + await agent.api.app.bsky.feed.getAuthorFeed( + { actor: alice }, + { headers: sc.getHeaders(carol) }, + ) + } catch (e: any) { + expect(e.message).toEqual('Profile not found') + } // Cleanup await reverseModerationAction(action.id) From 40fde9585c500e7b5eef9655984e37f474593bbd Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 17 Aug 2023 14:02:56 -0500 Subject: [PATCH 152/237] fix bad test syntax --- packages/bsky/tests/views/author-feed.test.ts | 8 +++----- packages/pds/tests/views/author-feed.test.ts | 6 ++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index 59f5bac5e58..8e6c7b20ce7 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -165,14 +165,12 @@ describe('pds author feed views', () => { }, ) - try { - const { data: postBlock } = await agent.api.app.bsky.feed.getAuthorFeed( + expect(async () => { + await agent.api.app.bsky.feed.getAuthorFeed( { actor: alice }, { headers: await network.serviceHeaders(carol) }, ) - } catch (e: any) { - expect(e.message).toEqual('Profile not found') - } + }).rejects.toThrow('Profile not found') // Cleanup await agent.api.com.atproto.admin.reverseModerationAction( diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index 8e3e7a4071f..3e7ee8be947 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -197,14 +197,12 @@ describe('pds author feed views', () => { did: alice, }) - try { + expect(async () => { await agent.api.app.bsky.feed.getAuthorFeed( { actor: alice }, { headers: sc.getHeaders(carol) }, ) - } catch (e: any) { - expect(e.message).toEqual('Profile not found') - } + }).rejects.toThrow('Profile not found') // Cleanup await reverseModerationAction(action.id) From 30cb6412cee9db552711b2232a8d7b5bd17cda92 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 17 Aug 2023 13:14:14 -0500 Subject: [PATCH 153/237] include reposted replies in posts_no_replies filter (cherry picked from commit 588cf2562cc61441c170fec4f2354eedee39c4f6) --- .../src/api/app/bsky/feed/getAuthorFeed.ts | 11 ++++++-- packages/bsky/tests/views/author-feed.test.ts | 28 +++++++++++++++---- .../api/app/bsky/feed/getAuthorFeed.ts | 11 ++++++-- packages/pds/tests/views/author-feed.test.ts | 23 +++++++++++++-- 4 files changed, 61 insertions(+), 12 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 6bc6bebe046..48d8af533ef 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -59,8 +59,15 @@ export default function (server: Server, ctx: AppContext) { .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'), ) } else if (filter === 'posts_no_replies') { - // only posts, no replies - feedItemsQb = feedItemsQb.where('post.replyParent', 'is', null) + feedItemsQb = feedItemsQb + // only posts, no replies + .where('post.replyParent', 'is', null) + // or any reposted replies + .orWhere((qb) => { + return qb + .where('originatorDid', '=', did) + .where('type', '=', 'repost') + }) } if (viewer !== null) { diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index 8e6c7b20ce7..8e5fcc509f1 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -274,13 +274,31 @@ describe('pds author feed views', () => { }) it('filters by posts_no_replies', async () => { - const { data: postsOnlyFeed } = await agent.api.app.bsky.feed.getAuthorFeed( - { actor: carol, filter: 'posts_no_replies' }, - ) + const { data: carolFeed } = await agent.api.app.bsky.feed.getAuthorFeed({ + actor: carol, + filter: 'posts_no_replies', + }) + + expect( + carolFeed.feed.every(({ post }) => { + return ( + (isRecord(post.record) && !post.record.reply) || + (isRecord(post.record) && post.record.reply) + ) + }), + ).toBeTruthy() + + const { data: danFeed } = await agent.api.app.bsky.feed.getAuthorFeed({ + actor: dan, + filter: 'posts_no_replies', + }) expect( - postsOnlyFeed.feed.every(({ post }) => { - return isRecord(post.record) && !post.record.reply + danFeed.feed.every(({ post }) => { + return ( + (isRecord(post.record) && !post.record.reply) || + (isRecord(post.record) && post.record.reply) + ) }), ).toBeTruthy() }) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index 2c88f774804..0b5a671d116 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -64,8 +64,15 @@ export default function (server: Server, ctx: AppContext) { .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'), ) } else if (params.filter === 'posts_no_replies') { - // only posts, no replies - feedItemsQb = feedItemsQb.where('post.replyParent', 'is', null) + feedItemsQb = feedItemsQb + // only posts, no replies + .where('post.replyParent', 'is', null) + // or any reposted replies + .orWhere((qb) => { + return qb + .where('originatorDid', '=', actorDid) + .where('type', '=', 'repost') + }) } // for access-based auth, enforce blocks and mutes diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index 3e7ee8be947..94f63257cfc 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -334,14 +334,31 @@ describe('pds author feed views', () => { }) it('filters by posts_no_replies', async () => { - const { data: postsOnlyFeed } = await agent.api.app.bsky.feed.getAuthorFeed( + const { data: carolFeed } = await agent.api.app.bsky.feed.getAuthorFeed( { actor: carol, filter: 'posts_no_replies' }, { headers: sc.getHeaders(alice) }, ) expect( - postsOnlyFeed.feed.every(({ post }) => { - return isRecord(post.record) && !post.record.reply + carolFeed.feed.every(({ post }) => { + return ( + (isRecord(post.record) && !post.record.reply) || + (isRecord(post.record) && post.record.reply) + ) + }), + ).toBeTruthy() + + const { data: danFeed } = await agent.api.app.bsky.feed.getAuthorFeed( + { actor: dan, filter: 'posts_no_replies' }, + { headers: sc.getHeaders(alice) }, + ) + + expect( + danFeed.feed.every(({ post }) => { + return ( + (isRecord(post.record) && !post.record.reply) || + (isRecord(post.record) && post.record.reply) + ) }), ).toBeTruthy() }) From 377a9721b933a690bd415d3ca65ecc8e392ec9c1 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 17 Aug 2023 14:20:05 -0500 Subject: [PATCH 154/237] fix typos --- packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts | 2 +- packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 48d8af533ef..512d8a3bd34 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -65,7 +65,7 @@ export default function (server: Server, ctx: AppContext) { // or any reposted replies .orWhere((qb) => { return qb - .where('originatorDid', '=', did) + .where('originatorDid', '=', actorDid) .where('type', '=', 'repost') }) } diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index 0b5a671d116..762471aa90d 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -1,4 +1,3 @@ -import { sql } from 'kysely' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../../lexicon' import { FeedKeyset } from '../util/feed' From 2e90124f7269f3e8f1503d630dff3d9383284a29 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 17 Aug 2023 15:02:50 -0500 Subject: [PATCH 155/237] simplify query --- packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts | 9 +++------ .../pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts | 8 +++----- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 512d8a3bd34..0cef444881a 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -60,13 +60,10 @@ export default function (server: Server, ctx: AppContext) { ) } else if (filter === 'posts_no_replies') { feedItemsQb = feedItemsQb - // only posts, no replies - .where('post.replyParent', 'is', null) - // or any reposted replies - .orWhere((qb) => { + .where((qb) => { return qb - .where('originatorDid', '=', actorDid) - .where('type', '=', 'repost') + .where('post.replyParent', 'is', null) + .orWhere('type', '=', 'repost') }) } diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index 762471aa90d..cece01aa2d9 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -65,12 +65,10 @@ export default function (server: Server, ctx: AppContext) { } else if (params.filter === 'posts_no_replies') { feedItemsQb = feedItemsQb // only posts, no replies - .where('post.replyParent', 'is', null) - // or any reposted replies - .orWhere((qb) => { + .where((qb) => { return qb - .where('originatorDid', '=', actorDid) - .where('type', '=', 'repost') + .where('post.replyParent', 'is', null) + .orWhere('type', '=', 'repost') }) } From 0ea353b21ff11c3440022f8483701fe56d8f2b02 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 17 Aug 2023 15:30:30 -0500 Subject: [PATCH 156/237] add repost of reply, update snaps --- .../tests/__snapshots__/indexing.test.ts.snap | 2 +- packages/bsky/tests/seeds/basic.ts | 3 +- .../__snapshots__/author-feed.test.ts.snap | 248 ++- .../__snapshots__/notifications.test.ts.snap | 362 ++-- .../views/__snapshots__/posts.test.ts.snap | 2 +- .../views/__snapshots__/thread.test.ts.snap | 10 +- .../views/__snapshots__/timeline.test.ts.snap | 1105 ++++++++--- .../bsky/tests/views/notifications.test.ts | 12 +- .../__snapshots__/feedgen.test.ts.snap | 2 +- .../timeline-skeleton.test.ts.snap | 580 ++++-- packages/pds/tests/seeds/basic.ts | 3 +- .../__snapshots__/author-feed.test.ts.snap | 277 ++- .../__snapshots__/notifications.test.ts.snap | 437 +++-- .../views/__snapshots__/posts.test.ts.snap | 2 +- .../views/__snapshots__/thread.test.ts.snap | 14 +- .../views/__snapshots__/timeline.test.ts.snap | 1705 ++++++++++++----- .../pds/tests/views/notifications.test.ts | 14 +- 17 files changed, 3476 insertions(+), 1302 deletions(-) diff --git a/packages/bsky/tests/__snapshots__/indexing.test.ts.snap b/packages/bsky/tests/__snapshots__/indexing.test.ts.snap index 8fb7e5cf193..9abbe8a3f64 100644 --- a/packages/bsky/tests/__snapshots__/indexing.test.ts.snap +++ b/packages/bsky/tests/__snapshots__/indexing.test.ts.snap @@ -76,7 +76,7 @@ Array [ "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, + "repostCount": 1, "uri": "record(1)", "viewer": Object {}, }, diff --git a/packages/bsky/tests/seeds/basic.ts b/packages/bsky/tests/seeds/basic.ts index d0443e04d54..c1bd7e41e09 100644 --- a/packages/bsky/tests/seeds/basic.ts +++ b/packages/bsky/tests/seeds/basic.ts @@ -117,7 +117,7 @@ export default async (sc: SeedClient, users = true) => { sc.posts[alice][1].ref, replies.carol[0], ) - await sc.reply( + const alicesReplyToBob = await sc.reply( alice, sc.posts[alice][1].ref, sc.replies[bob][0].ref, @@ -125,6 +125,7 @@ export default async (sc: SeedClient, users = true) => { ) await sc.repost(carol, sc.posts[dan][1].ref) await sc.repost(dan, sc.posts[alice][1].ref) + await sc.repost(dan, alicesReplyToBob.ref) return sc } diff --git a/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap b/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap index 67ee62e1336..59123e54b20 100644 --- a/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap @@ -52,7 +52,7 @@ Array [ "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, + "repostCount": 1, "uri": "record(0)", "viewer": Object {}, }, @@ -1168,6 +1168,208 @@ Array [ "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(4)", + "uri": "record(4)", + }, + "root": Object { + "cid": "cids(3)", + "uri": "record(3)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object { + "repost": "record(5)", + }, + }, + "reason": Object { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": Object { + "did": "user(2)", + "handle": "dan.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", + "did": "user(3)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "following": "record(8)", + "muted": false, + }, + }, + "cid": "cids(4)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(4)", + "val": "test-label", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(4)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(5)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(3)", + "uri": "record(3)", + }, + "root": Object { + "cid": "cids(3)", + "uri": "record(3)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(4)", + "viewer": Object {}, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(1)", + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(3)", + "viewer": Object { + "like": "record(7)", + "repost": "record(6)", + }, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(1)", + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], "likeCount": 3, "record": Object { "$type": "app.bsky.feed.post", @@ -1176,10 +1378,10 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(3)", "viewer": Object { - "like": "record(4)", - "repost": "record(3)", + "like": "record(7)", + "repost": "record(6)", }, }, "reason": Object { @@ -1207,13 +1409,13 @@ Array [ "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(6)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(3)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -1221,7 +1423,7 @@ Array [ "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(7)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -1235,8 +1437,8 @@ Array [ }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)@jpeg", + "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", }, ], }, @@ -1244,8 +1446,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(4)/cids(1)@jpeg", + "did": "user(3)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -1255,10 +1457,10 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(7)", + "uri": "record(11)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1274,7 +1476,7 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(6)", + "uri": "record(10)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1300,7 +1502,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(6)", + "$link": "cids(8)", }, "size": 12736, }, @@ -1309,8 +1511,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(7)", - "uri": "record(7)", + "cid": "cids(9)", + "uri": "record(11)", }, }, }, @@ -1327,8 +1529,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(4)", - "uri": "record(6)", + "cid": "cids(7)", + "uri": "record(10)", }, }, "facets": Array [ @@ -1349,7 +1551,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(5)", + "uri": "record(9)", "viewer": Object {}, }, }, @@ -1364,7 +1566,7 @@ Array [ "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1375,7 +1577,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(9)", + "uri": "record(12)", "viewer": Object {}, }, }, @@ -1436,7 +1638,7 @@ Array [ "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, + "repostCount": 1, "uri": "record(0)", "viewer": Object {}, }, diff --git a/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap b/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap index fd8c6b0ff3c..71c16d7420f 100644 --- a/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap @@ -44,17 +44,44 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], + "reason": "repost", + "reasonSubject": "record(4)", + "record": Object { + "$type": "app.bsky.feed.repost", + "createdAt": "1970-01-01T00:00:00.000Z", + "subject": Object { + "cid": "cids(3)", + "uri": "record(4)", + }, + }, + "uri": "record(3)", + }, + Object { + "author": Object { + "did": "user(0)", + "handle": "dan.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "following": "record(1)", + "muted": false, + }, + }, + "cid": "cids(4)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "isRead": true, + "labels": Array [], "reason": "like", - "reasonSubject": "record(2)", + "reasonSubject": "record(4)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(1)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(4)", }, }, - "uri": "record(3)", + "uri": "record(5)", }, Object { "author": Object { @@ -63,14 +90,14 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(5)", + "followedBy": "record(6)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(5)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "follow", "record": Object { @@ -78,7 +105,7 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "subject": "user(2)", }, - "uri": "record(4)", + "uri": "record(6)", }, Object { "author": Object { @@ -87,33 +114,33 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(5)", + "followedBy": "record(6)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "reply", - "reasonSubject": "record(7)", + "reasonSubject": "record(2)", "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(5)", - "uri": "record(7)", - }, - "root": Object { "cid": "cids(1)", "uri": "record(2)", }, + "root": Object { + "cid": "cids(3)", + "uri": "record(4)", + }, }, "text": "indeed", }, - "uri": "record(6)", + "uri": "record(8)", }, Object { "author": Object { @@ -122,26 +149,26 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(5)", + "followedBy": "record(6)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "like", - "reasonSubject": "record(9)", + "reasonSubject": "record(10)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(7)", - "uri": "record(9)", + "cid": "cids(8)", + "uri": "record(10)", }, }, - "uri": "record(8)", + "uri": "record(9)", }, Object { "author": Object { @@ -150,30 +177,30 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(5)", + "followedBy": "record(6)", + "following": "record(7)", "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "like", - "reasonSubject": "record(2)", + "reasonSubject": "record(4)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(1)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(4)", }, }, - "uri": "record(10)", + "uri": "record(11)", }, Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(10)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(11)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -182,14 +209,14 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(11)", - "following": "record(12)", + "followedBy": "record(12)", + "following": "record(13)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "follow", "record": Object { @@ -197,11 +224,11 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "subject": "user(2)", }, - "uri": "record(11)", + "uri": "record(12)", }, Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(10)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(11)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -210,34 +237,34 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(11)", - "following": "record(12)", + "followedBy": "record(12)", + "following": "record(13)", "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [ Object { - "cid": "cids(11)", + "cid": "cids(12)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(13)", + "uri": "record(14)", "val": "test-label", }, Object { - "cid": "cids(11)", + "cid": "cids(12)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(13)", + "uri": "record(14)", "val": "test-label-2", }, ], "reason": "reply", - "reasonSubject": "record(2)", + "reasonSubject": "record(4)", "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -250,7 +277,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(12)", + "$link": "cids(13)", }, "size": 4114, }, @@ -259,21 +286,21 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(1)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(4)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "text": "hear that label_me label_me_2", }, - "uri": "record(13)", + "uri": "record(14)", }, Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(10)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(11)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -282,30 +309,30 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(11)", - "following": "record(12)", + "followedBy": "record(12)", + "following": "record(13)", "muted": false, }, }, - "cid": "cids(13)", + "cid": "cids(14)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "like", - "reasonSubject": "record(9)", + "reasonSubject": "record(10)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(7)", - "uri": "record(9)", + "cid": "cids(8)", + "uri": "record(10)", }, }, - "uri": "record(14)", + "uri": "record(15)", }, Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(10)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(11)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -314,26 +341,26 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(11)", - "following": "record(12)", + "followedBy": "record(12)", + "following": "record(13)", "muted": false, }, }, - "cid": "cids(14)", + "cid": "cids(15)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "like", - "reasonSubject": "record(2)", + "reasonSubject": "record(4)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(1)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(4)", }, }, - "uri": "record(15)", + "uri": "record(16)", }, ] `; @@ -382,6 +409,33 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], + "reason": "repost", + "reasonSubject": "record(4)", + "record": Object { + "$type": "app.bsky.feed.repost", + "createdAt": "1970-01-01T00:00:00.000Z", + "subject": Object { + "cid": "cids(3)", + "uri": "record(4)", + }, + }, + "uri": "record(3)", + }, + Object { + "author": Object { + "did": "user(0)", + "handle": "dan.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "following": "record(1)", + "muted": false, + }, + }, + "cid": "cids(4)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "isRead": false, + "labels": Array [], "reason": "mention", "record": Object { "$type": "app.bsky.feed.post", @@ -389,8 +443,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(3)", - "uri": "record(4)", + "cid": "cids(5)", + "uri": "record(6)", }, }, "facets": Array [ @@ -409,7 +463,7 @@ Array [ ], "text": "@alice.bluesky.xyz is the best", }, - "uri": "record(3)", + "uri": "record(5)", }, Object { "author": Object { @@ -422,21 +476,21 @@ Array [ "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "like", - "reasonSubject": "record(2)", + "reasonSubject": "record(4)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(1)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(4)", }, }, - "uri": "record(5)", + "uri": "record(7)", }, Object { "author": Object { @@ -445,12 +499,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(7)", + "followedBy": "record(8)", + "following": "record(9)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -460,7 +514,7 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "subject": "user(1)", }, - "uri": "record(6)", + "uri": "record(8)", }, Object { "author": Object { @@ -469,33 +523,33 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(7)", + "followedBy": "record(8)", + "following": "record(9)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "reply", - "reasonSubject": "record(9)", + "reasonSubject": "record(2)", "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(7)", - "uri": "record(9)", - }, - "root": Object { "cid": "cids(1)", "uri": "record(2)", }, + "root": Object { + "cid": "cids(3)", + "uri": "record(4)", + }, }, "text": "indeed", }, - "uri": "record(8)", + "uri": "record(10)", }, Object { "author": Object { @@ -504,33 +558,33 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(7)", + "followedBy": "record(8)", + "following": "record(9)", "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "reply", - "reasonSubject": "record(2)", + "reasonSubject": "record(4)", "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(1)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(4)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "text": "of course", }, - "uri": "record(10)", + "uri": "record(11)", }, Object { "author": Object { @@ -539,26 +593,26 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(7)", + "followedBy": "record(8)", + "following": "record(9)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "like", - "reasonSubject": "record(12)", + "reasonSubject": "record(13)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(10)", - "uri": "record(12)", + "cid": "cids(11)", + "uri": "record(13)", }, }, - "uri": "record(11)", + "uri": "record(12)", }, Object { "author": Object { @@ -567,30 +621,30 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(7)", + "followedBy": "record(8)", + "following": "record(9)", "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "like", - "reasonSubject": "record(2)", + "reasonSubject": "record(4)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(1)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(4)", }, }, - "uri": "record(13)", + "uri": "record(14)", }, Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(13)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(14)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -599,12 +653,12 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(14)", - "following": "record(15)", + "followedBy": "record(15)", + "following": "record(16)", "muted": false, }, }, - "cid": "cids(12)", + "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -614,11 +668,11 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "subject": "user(1)", }, - "uri": "record(14)", + "uri": "record(15)", }, Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(13)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(14)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -627,34 +681,34 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(14)", - "following": "record(15)", + "followedBy": "record(15)", + "following": "record(16)", "muted": false, }, }, - "cid": "cids(14)", + "cid": "cids(15)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [ Object { - "cid": "cids(14)", + "cid": "cids(15)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(16)", + "uri": "record(17)", "val": "test-label", }, Object { - "cid": "cids(14)", + "cid": "cids(15)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(16)", + "uri": "record(17)", "val": "test-label-2", }, ], "reason": "reply", - "reasonSubject": "record(2)", + "reasonSubject": "record(4)", "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -667,7 +721,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(15)", + "$link": "cids(16)", }, "size": 4114, }, @@ -676,21 +730,21 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(1)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(4)", }, "root": Object { - "cid": "cids(1)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(4)", }, }, "text": "hear that label_me label_me_2", }, - "uri": "record(16)", + "uri": "record(17)", }, Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(13)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(14)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -699,30 +753,30 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(14)", - "following": "record(15)", + "followedBy": "record(15)", + "following": "record(16)", "muted": false, }, }, - "cid": "cids(16)", + "cid": "cids(17)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "like", - "reasonSubject": "record(12)", + "reasonSubject": "record(13)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(10)", - "uri": "record(12)", + "cid": "cids(11)", + "uri": "record(13)", }, }, - "uri": "record(17)", + "uri": "record(18)", }, Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(13)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(14)@jpeg", "description": "hi im bob label_me", "did": "user(3)", "displayName": "bobby", @@ -731,26 +785,26 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(14)", - "following": "record(15)", + "followedBy": "record(15)", + "following": "record(16)", "muted": false, }, }, - "cid": "cids(17)", + "cid": "cids(18)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "like", - "reasonSubject": "record(2)", + "reasonSubject": "record(4)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(1)", - "uri": "record(2)", + "cid": "cids(3)", + "uri": "record(4)", }, }, - "uri": "record(18)", + "uri": "record(19)", }, ] `; diff --git a/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap b/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap index 5e9e61e52ad..df8a4cdf826 100644 --- a/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/posts.test.ts.snap @@ -459,7 +459,7 @@ Array [ "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, + "repostCount": 1, "uri": "record(12)", "viewer": Object {}, }, diff --git a/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap b/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap index f9aba1e54a0..5886ed56019 100644 --- a/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap @@ -188,7 +188,7 @@ Object { "text": "thanks bob", }, "replyCount": 0, - "repostCount": 1, + "repostCount": 2, "uri": "record(0)", "viewer": Object { "repost": "record(6)", @@ -423,7 +423,7 @@ Object { "text": "thanks bob", }, "replyCount": 0, - "repostCount": 1, + "repostCount": 2, "uri": "record(8)", "viewer": Object { "repost": "record(9)", @@ -943,7 +943,7 @@ Object { "text": "thanks bob", }, "replyCount": 0, - "repostCount": 1, + "repostCount": 2, "uri": "record(0)", "viewer": Object { "repost": "record(6)", @@ -1012,7 +1012,7 @@ Object { "text": "thanks bob", }, "replyCount": 0, - "repostCount": 1, + "repostCount": 2, "uri": "record(0)", "viewer": Object { "repost": "record(6)", @@ -1208,7 +1208,7 @@ Object { "text": "thanks bob", }, "replyCount": 0, - "repostCount": 1, + "repostCount": 2, "uri": "record(6)", "viewer": Object { "repost": "record(7)", diff --git a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap index 417015173d3..fe9b243c10a 100644 --- a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap @@ -35,6 +35,75 @@ Array [ "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(4)", + "uri": "record(3)", + }, + "root": Object { + "cid": "cids(3)", + "uri": "record(2)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object {}, + }, + "reason": Object { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": Object { + "did": "user(2)", + "handle": "dan.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "following": "record(4)", + "muted": false, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], "likeCount": 3, "record": Object { "$type": "app.bsky.feed.post", @@ -43,7 +112,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, "reason": Object { @@ -54,7 +123,7 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -91,7 +160,7 @@ Array [ "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -101,18 +170,18 @@ Array [ "reply": Object { "parent": Object { "cid": "cids(4)", - "uri": "record(4)", + "uri": "record(3)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(3)", + "uri": "record(2)", }, }, "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, - "uri": "record(3)", + "repostCount": 1, + "uri": "record(0)", "viewer": Object {}, }, }, @@ -157,7 +226,7 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -263,7 +332,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -274,7 +343,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -286,7 +355,7 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -342,7 +411,7 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -461,6 +530,75 @@ Array [ "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(4)", + "uri": "record(3)", + }, + "root": Object { + "cid": "cids(3)", + "uri": "record(2)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object {}, + }, + "reason": Object { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": Object { + "did": "user(2)", + "handle": "dan.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "following": "record(4)", + "muted": false, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], "likeCount": 3, "record": Object { "$type": "app.bsky.feed.post", @@ -469,7 +607,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, "reason": Object { @@ -480,7 +618,7 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -517,7 +655,7 @@ Array [ "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -527,18 +665,18 @@ Array [ "reply": Object { "parent": Object { "cid": "cids(4)", - "uri": "record(4)", + "uri": "record(3)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(3)", + "uri": "record(2)", }, }, "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, - "uri": "record(3)", + "repostCount": 1, + "uri": "record(0)", "viewer": Object {}, }, }, @@ -564,12 +702,12 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(3)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(3)", + "uri": "record(2)", }, }, "text": "of course", @@ -610,7 +748,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -621,7 +759,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, "root": Object { @@ -654,7 +792,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -665,7 +803,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -799,7 +937,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -810,7 +948,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -822,7 +960,7 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -1048,43 +1186,240 @@ Array [ "muted": false, }, }, - "cid": "cids(14)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(14)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "user(0)", - "uri": "record(17)", - "val": "self-label", - }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "labels": Object { - "$type": "com.atproto.label.defs#selfLabels", - "values": Array [ + "cid": "cids(14)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(14)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(17)", + "val": "self-label", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "labels": Object { + "$type": "com.atproto.label.defs#selfLabels", + "values": Array [ + Object { + "val": "self-label", + }, + ], + }, + "text": "hey there", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(17)", + "viewer": Object {}, + }, + }, +] +`; + +exports[`timeline views fetches authenticated user's home feed w/ reverse-chronological algorithm 1`] = ` +Array [ + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(4)", + "uri": "record(3)", + }, + "root": Object { + "cid": "cids(3)", + "uri": "record(2)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object {}, + }, + "reason": Object { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": Object { + "did": "user(2)", + "handle": "dan.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "following": "record(4)", + "muted": false, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", + "did": "user(3)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(6)", + "following": "record(5)", + "muted": false, + }, + }, + "cid": "cids(4)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(3)", + "val": "test-label", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(3)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(5)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(3)", + "uri": "record(2)", + }, + "root": Object { + "cid": "cids(3)", + "uri": "record(2)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(3)", + "viewer": Object {}, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ Object { - "val": "self-label", + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", }, ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, }, - "text": "hey there", + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(2)", + "viewer": Object {}, }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(17)", - "viewer": Object {}, }, }, -] -`; - -exports[`timeline views fetches authenticated user's home feed w/ reverse-chronological algorithm 1`] = ` -Array [ Object { "post": Object { "author": Object { @@ -1115,7 +1450,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1126,7 +1461,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, "reason": Object { @@ -1137,7 +1472,7 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -1152,27 +1487,27 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(6)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(3)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(10)", + "following": "record(9)", "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(7)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -1186,8 +1521,8 @@ Array [ }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)@jpeg", + "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", }, ], }, @@ -1195,31 +1530,31 @@ 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(4)/cids(1)@jpeg", + "did": "user(3)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(7)", + "uri": "record(11)", "val": "kind", }, ], - "uri": "record(7)", + "uri": "record(11)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1236,15 +1571,15 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(4)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(8)", "val": "kind", }, ], - "uri": "record(4)", + "uri": "record(8)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1270,7 +1605,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(6)", + "$link": "cids(8)", }, "size": 12736, }, @@ -1279,8 +1614,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(7)", - "uri": "record(7)", + "cid": "cids(9)", + "uri": "record(11)", }, }, }, @@ -1297,8 +1632,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(4)", - "uri": "record(4)", + "cid": "cids(7)", + "uri": "record(8)", }, }, "facets": Array [ @@ -1319,19 +1654,19 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(3)", + "uri": "record(7)", "viewer": Object {}, }, "reason": Object { "$type": "app.bsky.feed.defs#reasonRepost", "by": Object { - "did": "user(3)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(10)", + "following": "record(9)", "muted": false, }, }, @@ -1368,7 +1703,7 @@ Array [ "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1377,64 +1712,64 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(9)", - "uri": "record(11)", + "cid": "cids(4)", + "uri": "record(3)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(3)", + "uri": "record(2)", }, }, "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, - "uri": "record(10)", + "repostCount": 1, + "uri": "record(0)", "viewer": Object {}, }, "reply": Object { "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", - "did": "user(4)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", + "did": "user(3)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(4)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(5)@jpeg", + "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", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(9)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(11)", + "uri": "record(3)", "val": "test-label", }, Object { - "cid": "cids(9)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(11)", + "uri": "record(3)", "val": "test-label-2", }, ], @@ -1460,19 +1795,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(3)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(3)", + "uri": "record(2)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(11)", + "uri": "record(3)", "viewer": Object {}, }, "root": Object { @@ -1505,7 +1840,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1516,7 +1851,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -1524,13 +1859,13 @@ Array [ Object { "post": Object { "author": Object { - "did": "user(3)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(10)", + "following": "record(9)", "muted": false, }, }, @@ -1543,12 +1878,12 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(3)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(3)", + "uri": "record(2)", }, }, "text": "of course", @@ -1589,7 +1924,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1600,7 +1935,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, "root": Object { @@ -1633,7 +1968,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1644,7 +1979,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -1652,45 +1987,45 @@ 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(4)/cids(1)@jpeg", + "did": "user(3)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(4)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(5)@jpeg", + "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", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(9)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(11)", + "uri": "record(3)", "val": "test-label", }, Object { - "cid": "cids(9)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(11)", + "uri": "record(3)", "val": "test-label-2", }, ], @@ -1716,19 +2051,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(3)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(3)", + "uri": "record(2)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(11)", + "uri": "record(3)", "viewer": Object {}, }, "reply": Object { @@ -1762,7 +2097,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1773,7 +2108,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, "root": Object { @@ -1806,7 +2141,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1817,7 +2152,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -1863,40 +2198,40 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(6)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(3)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(10)", + "following": "record(9)", "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(4)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(8)", "val": "kind", }, ], - "uri": "record(4)", + "uri": "record(8)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1922,7 +2257,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(6)", + "$link": "cids(8)", }, "size": 12736, }, @@ -1931,8 +2266,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(7)", - "uri": "record(7)", + "cid": "cids(9)", + "uri": "record(11)", }, }, }, @@ -1943,15 +2278,15 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(3)", + "uri": "record(7)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(4)", - "uri": "record(4)", + "cid": "cids(7)", + "uri": "record(8)", }, }, "facets": Array [ @@ -1990,8 +2325,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(3)", - "uri": "record(3)", + "cid": "cids(6)", + "uri": "record(7)", }, }, "text": "yoohoo label_me", @@ -2005,15 +2340,15 @@ 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(4)/cids(1)@jpeg", + "did": "user(3)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, @@ -2062,7 +2397,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -2073,7 +2408,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -2085,27 +2420,27 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(6)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(3)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(10)", + "following": "record(9)", "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(7)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -2119,8 +2454,8 @@ Array [ }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)@jpeg", + "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", }, ], }, @@ -2128,31 +2463,31 @@ 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(4)/cids(1)@jpeg", + "did": "user(3)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(7)", + "uri": "record(11)", "val": "kind", }, ], - "uri": "record(7)", + "uri": "record(11)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2169,15 +2504,15 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(4)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(8)", "val": "kind", }, ], - "uri": "record(4)", + "uri": "record(8)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2203,7 +2538,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(6)", + "$link": "cids(8)", }, "size": 12736, }, @@ -2212,8 +2547,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(7)", - "uri": "record(7)", + "cid": "cids(9)", + "uri": "record(11)", }, }, }, @@ -2230,8 +2565,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(4)", - "uri": "record(4)", + "cid": "cids(7)", + "uri": "record(8)", }, }, "facets": Array [ @@ -2252,7 +2587,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(3)", + "uri": "record(7)", "viewer": Object {}, }, }, @@ -2264,7 +2599,7 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -2286,17 +2621,17 @@ Array [ Object { "post": Object { "author": Object { - "did": "user(3)", + "did": "user(5)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(10)", + "following": "record(9)", "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(7)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -2309,8 +2644,8 @@ Array [ }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(6)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(6)@jpeg", + "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", }, ], }, @@ -2318,32 +2653,32 @@ 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(4)/cids(1)@jpeg", + "did": "user(3)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "embeds": Array [], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(7)", + "uri": "record(11)", "val": "kind", }, ], - "uri": "record(7)", + "uri": "record(11)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2359,11 +2694,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(4)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(8)", "val": "kind", }, ], @@ -2393,7 +2728,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(6)", + "$link": "cids(8)", }, "size": 12736, }, @@ -2402,8 +2737,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(7)", - "uri": "record(7)", + "cid": "cids(9)", + "uri": "record(11)", }, }, }, @@ -2411,7 +2746,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(8)", "viewer": Object { "like": "record(16)", }, @@ -2420,27 +2755,27 @@ 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(4)/cids(1)@jpeg", + "did": "user(3)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(7)", + "uri": "record(11)", "val": "kind", }, ], @@ -2456,7 +2791,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(7)", + "uri": "record(11)", "viewer": Object {}, }, }, @@ -2769,7 +3104,7 @@ Array [ "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, + "repostCount": 1, "uri": "record(5)", "viewer": Object {}, }, @@ -3961,7 +4296,7 @@ Array [ "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, + "repostCount": 1, "uri": "record(5)", "viewer": Object {}, }, @@ -4693,6 +5028,208 @@ Array [ "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(4)", + "uri": "record(4)", + }, + "root": Object { + "cid": "cids(3)", + "uri": "record(3)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object { + "repost": "record(5)", + }, + }, + "reason": Object { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": Object { + "did": "user(2)", + "handle": "dan.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", + "did": "user(3)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "following": "record(8)", + "muted": false, + }, + }, + "cid": "cids(4)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(5)@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(4)", + "val": "test-label", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(4)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(5)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(3)", + "uri": "record(3)", + }, + "root": Object { + "cid": "cids(3)", + "uri": "record(3)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(4)", + "viewer": Object {}, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(1)", + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(3)", + "viewer": Object { + "like": "record(7)", + "repost": "record(6)", + }, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(1)", + "muted": false, + }, + }, + "cid": "cids(3)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], "likeCount": 3, "record": Object { "$type": "app.bsky.feed.post", @@ -4701,10 +5238,10 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(3)", "viewer": Object { - "like": "record(4)", - "repost": "record(3)", + "like": "record(7)", + "repost": "record(6)", }, }, "reason": Object { @@ -4731,37 +5268,37 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(6)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(4)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(4)@jpeg", + "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", }, ], }, "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(5)", + "uri": "record(4)", "val": "test-label", }, Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(5)", + "uri": "record(4)", "val": "test-label-2", }, ], @@ -4778,7 +5315,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -4787,19 +5324,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(3)", + "uri": "record(3)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(3)", + "uri": "record(3)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(5)", + "uri": "record(4)", "viewer": Object {}, }, "reply": Object { @@ -4834,7 +5371,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -4845,10 +5382,10 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(3)", "viewer": Object { - "like": "record(4)", - "repost": "record(3)", + "like": "record(7)", + "repost": "record(6)", }, }, "root": Object { @@ -4882,7 +5419,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(3)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -4893,10 +5430,10 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(3)", "viewer": Object { - "like": "record(4)", - "repost": "record(3)", + "like": "record(7)", + "repost": "record(6)", }, }, }, @@ -4911,11 +5448,11 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(6)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -4926,7 +5463,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(7)", + "uri": "record(9)", "viewer": Object {}, }, }, @@ -4941,7 +5478,7 @@ Array [ "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -4955,7 +5492,7 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -4964,13 +5501,13 @@ Array [ "images": Array [ Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(6)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(6)/cids(5)@jpeg", }, Object { "alt": "tests/image/fixtures/key-alt.jpg", - "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", + "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", }, ], }, @@ -4985,23 +5522,23 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(6)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(9)", + "cid": "cids(10)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(10)", + "uri": "record(12)", "val": "kind", }, ], - "uri": "record(10)", + "uri": "record(12)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -5018,15 +5555,15 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(8)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(9)", + "uri": "record(11)", "val": "kind", }, ], - "uri": "record(9)", + "uri": "record(11)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -5041,7 +5578,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -5052,7 +5589,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(8)", + "$link": "cids(9)", }, "size": 12736, }, @@ -5061,8 +5598,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(9)", - "uri": "record(10)", + "cid": "cids(10)", + "uri": "record(12)", }, }, }, @@ -5079,8 +5616,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(7)", - "uri": "record(9)", + "cid": "cids(8)", + "uri": "record(11)", }, }, "facets": Array [ @@ -5101,7 +5638,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object {}, }, }, @@ -5116,7 +5653,7 @@ Array [ "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(11)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -5127,7 +5664,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(11)", + "uri": "record(13)", "viewer": Object {}, }, }, @@ -5141,19 +5678,19 @@ Array [ "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(6)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(9)", + "cid": "cids(10)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(10)", + "uri": "record(12)", "val": "kind", }, ], @@ -5169,7 +5706,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(10)", + "uri": "record(12)", "viewer": Object {}, }, }, diff --git a/packages/bsky/tests/views/notifications.test.ts b/packages/bsky/tests/views/notifications.test.ts index 7bdf6c85666..4f83815d782 100644 --- a/packages/bsky/tests/views/notifications.test.ts +++ b/packages/bsky/tests/views/notifications.test.ts @@ -56,7 +56,7 @@ describe('notification views', () => { { headers: await network.serviceHeaders(alice) }, ) - expect(notifCountAlice.data.count).toBe(11) + expect(notifCountAlice.data.count).toBe(13) const notifCountBob = await agent.api.app.bsky.notification.getUnreadCount( {}, @@ -84,7 +84,7 @@ describe('notification views', () => { { headers: await network.serviceHeaders(alice) }, ) - expect(notifCountAlice.data.count).toBe(12) + expect(notifCountAlice.data.count).toBe(13) const notifCountBob = await agent.api.app.bsky.notification.getUnreadCount( {}, @@ -133,7 +133,7 @@ describe('notification views', () => { ) const notifs = notifRes.data.notifications - expect(notifs.length).toBe(12) + expect(notifs.length).toBe(13) const readStates = notifs.map((notif) => notif.isRead) expect(readStates).toEqual(notifs.map(() => false)) @@ -162,7 +162,7 @@ describe('notification views', () => { { headers: await network.serviceHeaders(alice) }, ) - expect(full.data.notifications.length).toEqual(12) + expect(full.data.notifications.length).toEqual(13) expect(results(paginatedAll)).toEqual(results([full.data])) }) @@ -266,9 +266,9 @@ describe('notification views', () => { ) const notifs = sort(notifRes.data.notifications) - expect(notifs.length).toBe(10) + expect(notifs.length).toBe(11) expect(forSnapshot(notifs)).toMatchSnapshot() - expect(notifCount.data.count).toBe(10) + expect(notifCount.data.count).toBe(11) // Cleanup await Promise.all( diff --git a/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap index 2028b5c84a4..0f9ce0b75eb 100644 --- a/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap @@ -54,7 +54,7 @@ Object { "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, + "repostCount": 1, "uri": "record(0)", "viewer": Object {}, }, diff --git a/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap index 399f897bb83..1bb947bb64c 100644 --- a/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/timeline-skeleton.test.ts.snap @@ -54,7 +54,7 @@ Object { "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, + "repostCount": 1, "uri": "record(0)", "viewer": Object {}, }, @@ -1080,6 +1080,228 @@ Object { "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(3)", + "uri": "record(3)", + }, + "root": Object { + "cid": "cids(2)", + "uri": "record(2)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object {}, + }, + "reason": Object { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": Object { + "did": "user(1)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(1)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(4)", + "muted": false, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(2)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(6)", + "following": "record(5)", + "muted": false, + }, + }, + "cid": "cids(3)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(3)", + "val": "test-label", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(3)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(5)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(2)", + "uri": "record(2)", + }, + "root": Object { + "cid": "cids(2)", + "uri": "record(2)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(3)", + "viewer": Object {}, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(2)", + "viewer": Object {}, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], "likeCount": 3, "record": Object { "$type": "app.bsky.feed.post", @@ -1088,7 +1310,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, "reason": Object { @@ -1107,7 +1329,7 @@ Object { ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -1130,27 +1352,27 @@ Object { ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(6)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(2)", + "did": "user(3)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(7)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -1174,38 +1396,38 @@ Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", + "did": "user(2)", "displayName": "bobby", "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-a", }, Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(7)", + "uri": "record(12)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1221,7 +1443,7 @@ Object { ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(4)", + "uri": "record(9)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1236,7 +1458,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1247,7 +1469,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(8)", }, "size": 12736, }, @@ -1256,8 +1478,8 @@ Object { }, "record": Object { "record": Object { - "cid": "cids(6)", - "uri": "record(7)", + "cid": "cids(9)", + "uri": "record(12)", }, }, }, @@ -1274,8 +1496,8 @@ Object { "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(3)", - "uri": "record(4)", + "cid": "cids(7)", + "uri": "record(9)", }, }, "facets": Array [ @@ -1296,19 +1518,19 @@ Object { }, "replyCount": 0, "repostCount": 1, - "uri": "record(3)", + "uri": "record(8)", "viewer": Object {}, }, "reason": Object { "$type": "app.bsky.feed.defs#reasonRepost", "by": Object { - "did": "user(2)", + "did": "user(3)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, @@ -1345,7 +1567,7 @@ Object { "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1354,19 +1576,19 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(9)", - "uri": "record(12)", + "cid": "cids(3)", + "uri": "record(3)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, - "uri": "record(11)", + "repostCount": 1, + "uri": "record(0)", "viewer": Object {}, }, "reply": Object { @@ -1374,35 +1596,35 @@ Object { "$type": "app.bsky.feed.defs#postView", "author": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", + "did": "user(2)", "displayName": "bobby", "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-a", }, Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -1416,19 +1638,19 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(9)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(12)", + "uri": "record(3)", "val": "test-label", }, Object { - "cid": "cids(9)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(12)", + "uri": "record(3)", "val": "test-label-2", }, ], @@ -1445,7 +1667,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1454,19 +1676,19 @@ Object { }, "reply": Object { "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(12)", + "uri": "record(3)", "viewer": Object {}, }, "root": Object { @@ -1499,7 +1721,7 @@ Object { "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1510,7 +1732,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -1518,13 +1740,13 @@ Object { Object { "post": Object { "author": Object { - "did": "user(2)", + "did": "user(3)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, @@ -1537,12 +1759,12 @@ Object { "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "of course", @@ -1583,7 +1805,7 @@ Object { "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1594,7 +1816,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, "root": Object { @@ -1627,7 +1849,7 @@ Object { "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1638,7 +1860,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -1647,35 +1869,35 @@ Object { "post": Object { "author": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", + "did": "user(2)", "displayName": "bobby", "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-a", }, Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -1689,19 +1911,19 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(9)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(12)", + "uri": "record(3)", "val": "test-label", }, Object { - "cid": "cids(9)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(12)", + "uri": "record(3)", "val": "test-label-2", }, ], @@ -1718,7 +1940,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1727,19 +1949,19 @@ Object { }, "reply": Object { "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(12)", + "uri": "record(3)", "viewer": Object {}, }, "reply": Object { @@ -1773,7 +1995,7 @@ Object { "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1784,7 +2006,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, "root": Object { @@ -1817,7 +2039,7 @@ Object { "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1828,7 +2050,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -1882,31 +2104,31 @@ Object { ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(6)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(2)", + "did": "user(3)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(4)", + "uri": "record(9)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1921,7 +2143,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1932,7 +2154,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(8)", }, "size": 12736, }, @@ -1941,8 +2163,8 @@ Object { }, "record": Object { "record": Object { - "cid": "cids(6)", - "uri": "record(7)", + "cid": "cids(9)", + "uri": "record(12)", }, }, }, @@ -1953,15 +2175,15 @@ Object { ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(3)", + "uri": "record(8)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(3)", - "uri": "record(4)", + "cid": "cids(7)", + "uri": "record(9)", }, }, "facets": Array [ @@ -2000,8 +2222,8 @@ Object { "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(6)", + "uri": "record(8)", }, }, "text": "yoohoo label_me", @@ -2016,31 +2238,31 @@ Object { "post": Object { "author": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", + "did": "user(2)", "displayName": "bobby", "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-a", }, Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, @@ -2089,7 +2311,7 @@ Object { "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -2100,7 +2322,7 @@ Object { }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -2120,27 +2342,27 @@ Object { ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(6)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(2)", + "did": "user(3)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(7)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -2164,38 +2386,38 @@ Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", + "did": "user(2)", "displayName": "bobby", "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-a", }, Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(7)", + "uri": "record(12)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2211,7 +2433,7 @@ Object { ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(4)", + "uri": "record(9)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2226,7 +2448,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -2237,7 +2459,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(8)", }, "size": 12736, }, @@ -2246,8 +2468,8 @@ Object { }, "record": Object { "record": Object { - "cid": "cids(6)", - "uri": "record(7)", + "cid": "cids(9)", + "uri": "record(12)", }, }, }, @@ -2264,8 +2486,8 @@ Object { "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(3)", - "uri": "record(4)", + "cid": "cids(7)", + "uri": "record(9)", }, }, "facets": Array [ @@ -2286,7 +2508,7 @@ Object { }, "replyCount": 0, "repostCount": 1, - "uri": "record(3)", + "uri": "record(8)", "viewer": Object {}, }, }, @@ -2306,7 +2528,7 @@ Object { ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -2328,17 +2550,17 @@ Object { Object { "post": Object { "author": Object { - "did": "user(2)", + "did": "user(3)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(7)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -2361,39 +2583,39 @@ Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", + "did": "user(2)", "displayName": "bobby", "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-a", }, Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(9)", "embeds": Array [], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(7)", + "uri": "record(12)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2423,7 +2645,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -2434,7 +2656,7 @@ Object { "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(8)", }, "size": 12736, }, @@ -2443,8 +2665,8 @@ Object { }, "record": Object { "record": Object { - "cid": "cids(6)", - "uri": "record(7)", + "cid": "cids(9)", + "uri": "record(12)", }, }, }, @@ -2452,7 +2674,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(9)", "viewer": Object { "like": "record(17)", }, @@ -2462,35 +2684,35 @@ Object { "post": Object { "author": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", + "did": "user(2)", "displayName": "bobby", "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-a", }, Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -2505,7 +2727,7 @@ Object { }, "replyCount": 0, "repostCount": 0, - "uri": "record(7)", + "uri": "record(12)", "viewer": Object {}, }, }, diff --git a/packages/pds/tests/seeds/basic.ts b/packages/pds/tests/seeds/basic.ts index 464dd832975..ec8a5f05c00 100644 --- a/packages/pds/tests/seeds/basic.ts +++ b/packages/pds/tests/seeds/basic.ts @@ -119,7 +119,7 @@ export default async (sc: SeedClient, invite?: { code: string }) => { sc.posts[alice][1].ref, replies.carol[0], ) - await sc.reply( + const alicesReplyToBob = await sc.reply( alice, sc.posts[alice][1].ref, sc.replies[bob][0].ref, @@ -127,6 +127,7 @@ export default async (sc: SeedClient, invite?: { code: string }) => { ) await sc.repost(carol, sc.posts[dan][1].ref) await sc.repost(dan, sc.posts[alice][1].ref) + await sc.repost(dan, alicesReplyToBob.ref) await sc.agent.com.atproto.admin.takeModerationAction( { diff --git a/packages/pds/tests/views/__snapshots__/author-feed.test.ts.snap b/packages/pds/tests/views/__snapshots__/author-feed.test.ts.snap index 485ba8c2b1a..ca56abeb646 100644 --- a/packages/pds/tests/views/__snapshots__/author-feed.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/author-feed.test.ts.snap @@ -52,7 +52,7 @@ Array [ "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, + "repostCount": 1, "uri": "record(0)", "viewer": Object {}, }, @@ -1286,6 +1286,233 @@ Array [ "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(3)", + "uri": "record(4)", + }, + "root": Object { + "cid": "cids(2)", + "uri": "record(3)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object { + "repost": "record(5)", + }, + }, + "reason": Object { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": Object { + "did": "user(1)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(1)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(2)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(8)", + "muted": false, + }, + }, + "cid": "cids(3)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(4)", + "val": "test-label", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(4)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(5)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(2)", + "uri": "record(3)", + }, + "root": Object { + "cid": "cids(2)", + "uri": "record(3)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(4)", + "viewer": Object {}, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(1)", + "muted": false, + }, + }, + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(3)", + "viewer": Object { + "like": "record(7)", + "repost": "record(6)", + }, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(1)", + "muted": false, + }, + }, + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], "likeCount": 3, "record": Object { "$type": "app.bsky.feed.post", @@ -1294,10 +1521,10 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(3)", "viewer": Object { - "like": "record(4)", - "repost": "record(3)", + "like": "record(7)", + "repost": "record(6)", }, }, "reason": Object { @@ -1341,13 +1568,13 @@ Array [ "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(6)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(2)", + "did": "user(3)", "handle": "carol.test", "labels": Array [], "viewer": Object { @@ -1355,7 +1582,7 @@ Array [ "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(7)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -1379,23 +1606,23 @@ Array [ "$type": "app.bsky.embed.record#viewRecord", "author": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", + "did": "user(2)", "displayName": "bobby", "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "user(2)", "uri": "record(9)", "val": "self-label-a", }, Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", + "src": "user(2)", "uri": "record(9)", "val": "self-label-b", }, @@ -1406,10 +1633,10 @@ Array [ "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(7)", + "uri": "record(12)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1425,7 +1652,7 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(6)", + "uri": "record(11)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1440,7 +1667,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1451,7 +1678,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(8)", }, "size": 12736, }, @@ -1460,8 +1687,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(6)", - "uri": "record(7)", + "cid": "cids(9)", + "uri": "record(12)", }, }, }, @@ -1478,8 +1705,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(3)", - "uri": "record(6)", + "cid": "cids(7)", + "uri": "record(11)", }, }, "facets": Array [ @@ -1500,7 +1727,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(5)", + "uri": "record(10)", "viewer": Object {}, }, }, @@ -1523,7 +1750,7 @@ Array [ "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1534,7 +1761,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(10)", + "uri": "record(13)", "viewer": Object {}, }, }, @@ -1818,7 +2045,7 @@ Array [ "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, + "repostCount": 1, "uri": "record(0)", "viewer": Object {}, }, diff --git a/packages/pds/tests/views/__snapshots__/notifications.test.ts.snap b/packages/pds/tests/views/__snapshots__/notifications.test.ts.snap index bcf1adfe6d3..117bcdfbb79 100644 --- a/packages/pds/tests/views/__snapshots__/notifications.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/notifications.test.ts.snap @@ -61,6 +61,41 @@ Array [ "isRead": false, "labels": Array [], "reason": "repost", + "reasonSubject": "record(3)", + "record": Object { + "$type": "app.bsky.feed.repost", + "createdAt": "1970-01-01T00:00:00.000Z", + "subject": Object { + "cid": "cids(2)", + "uri": "record(3)", + }, + }, + "uri": "record(5)", + }, + Object { + "author": Object { + "did": "user(1)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(1)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(6)", + "muted": false, + }, + }, + "cid": "cids(4)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "isRead": false, + "labels": Array [], + "reason": "repost", "reasonSubject": "record(4)", "record": Object { "$type": "app.bsky.feed.repost", @@ -70,7 +105,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(5)", + "uri": "record(7)", }, Object { "author": Object { @@ -82,47 +117,47 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(10)", + "uri": "record(11)", "val": "self-label-a", }, Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(10)", + "uri": "record(11)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(10)", + "following": "record(9)", "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(5)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [ Object { - "cid": "cids(4)", + "cid": "cids(5)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(7)", + "uri": "record(8)", "val": "test-label", }, Object { - "cid": "cids(4)", + "cid": "cids(5)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(7)", + "uri": "record(8)", "val": "test-label-2", }, ], @@ -140,7 +175,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(6)", + "$link": "cids(7)", }, "size": 4114, }, @@ -159,7 +194,7 @@ Array [ }, "text": "hear that label_me label_me_2", }, - "uri": "record(7)", + "uri": "record(8)", }, Object { "author": Object { @@ -180,7 +215,7 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -194,7 +229,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(11)", + "uri": "record(12)", }, Object { "author": Object { @@ -208,21 +243,21 @@ Array [ "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "like", - "reasonSubject": "record(13)", + "reasonSubject": "record(14)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(9)", - "uri": "record(13)", + "cid": "cids(10)", + "uri": "record(14)", }, }, - "uri": "record(12)", + "uri": "record(13)", }, Object { "author": Object { @@ -236,7 +271,7 @@ Array [ "muted": false, }, }, - "cid": "cids(10)", + "cid": "cids(11)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -250,7 +285,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(14)", + "uri": "record(15)", }, Object { "author": Object { @@ -262,44 +297,44 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(10)", + "uri": "record(11)", "val": "self-label-a", }, Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(10)", + "uri": "record(11)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(10)", + "following": "record(9)", "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "like", - "reasonSubject": "record(13)", + "reasonSubject": "record(14)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(9)", - "uri": "record(13)", + "cid": "cids(10)", + "uri": "record(14)", }, }, - "uri": "record(15)", + "uri": "record(16)", }, Object { "author": Object { @@ -311,30 +346,30 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(10)", + "uri": "record(11)", "val": "self-label-a", }, Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(10)", + "uri": "record(11)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(10)", + "following": "record(9)", "muted": false, }, }, - "cid": "cids(12)", + "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -348,7 +383,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(16)", + "uri": "record(17)", }, Object { "author": Object { @@ -360,30 +395,30 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(10)", + "uri": "record(11)", "val": "self-label-a", }, Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(10)", + "uri": "record(11)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(10)", + "following": "record(9)", "muted": false, }, }, - "cid": "cids(13)", + "cid": "cids(14)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -393,7 +428,7 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "subject": "user(3)", }, - "uri": "record(9)", + "uri": "record(10)", }, Object { "author": Object { @@ -407,7 +442,7 @@ Array [ "muted": false, }, }, - "cid": "cids(14)", + "cid": "cids(15)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -720,6 +755,41 @@ Array [ "isRead": false, "labels": Array [], "reason": "repost", + "reasonSubject": "record(3)", + "record": Object { + "$type": "app.bsky.feed.repost", + "createdAt": "1970-01-01T00:00:00.000Z", + "subject": Object { + "cid": "cids(2)", + "uri": "record(3)", + }, + }, + "uri": "record(5)", + }, + Object { + "author": Object { + "did": "user(1)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(1)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(6)", + "muted": false, + }, + }, + "cid": "cids(4)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "isRead": false, + "labels": Array [], + "reason": "repost", "reasonSubject": "record(4)", "record": Object { "$type": "app.bsky.feed.repost", @@ -729,7 +799,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(5)", + "uri": "record(7)", }, Object { "author": Object { @@ -743,9 +813,9 @@ Array [ "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(5)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": false, + "isRead": true, "labels": Array [], "reason": "reply", "reasonSubject": "record(4)", @@ -764,7 +834,7 @@ Array [ }, "text": "of course", }, - "uri": "record(7)", + "uri": "record(8)", }, Object { "author": Object { @@ -776,47 +846,47 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(11)", + "uri": "record(12)", "val": "self-label-a", }, Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(11)", + "uri": "record(12)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [ Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(8)", + "uri": "record(9)", "val": "test-label", }, Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(8)", + "uri": "record(9)", "val": "test-label-2", }, ], @@ -834,7 +904,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(7)", + "$link": "cids(8)", }, "size": 4114, }, @@ -853,7 +923,7 @@ Array [ }, "text": "hear that label_me label_me_2", }, - "uri": "record(8)", + "uri": "record(9)", }, Object { "author": Object { @@ -874,7 +944,7 @@ Array [ "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [], @@ -888,7 +958,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(12)", + "uri": "record(13)", }, Object { "author": Object { @@ -902,21 +972,21 @@ Array [ "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [], "reason": "like", - "reasonSubject": "record(14)", + "reasonSubject": "record(15)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(10)", - "uri": "record(14)", + "cid": "cids(11)", + "uri": "record(15)", }, }, - "uri": "record(13)", + "uri": "record(14)", }, Object { "author": Object { @@ -930,7 +1000,7 @@ Array [ "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [], @@ -944,7 +1014,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(15)", + "uri": "record(16)", }, Object { "author": Object { @@ -956,44 +1026,44 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(11)", + "uri": "record(12)", "val": "self-label-a", }, Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(11)", + "uri": "record(12)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, - "cid": "cids(12)", + "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [], "reason": "like", - "reasonSubject": "record(14)", + "reasonSubject": "record(15)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(10)", - "uri": "record(14)", + "cid": "cids(11)", + "uri": "record(15)", }, }, - "uri": "record(16)", + "uri": "record(17)", }, Object { "author": Object { @@ -1005,30 +1075,30 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(11)", + "uri": "record(12)", "val": "self-label-a", }, Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(11)", + "uri": "record(12)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, - "cid": "cids(13)", + "cid": "cids(14)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [], @@ -1042,7 +1112,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(17)", + "uri": "record(18)", }, Object { "author": Object { @@ -1063,7 +1133,7 @@ Array [ "muted": false, }, }, - "cid": "cids(14)", + "cid": "cids(15)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [], @@ -1074,8 +1144,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(15)", - "uri": "record(19)", + "cid": "cids(16)", + "uri": "record(20)", }, }, "facets": Array [ @@ -1094,7 +1164,7 @@ Array [ ], "text": "@alice.bluesky.xyz is the best", }, - "uri": "record(18)", + "uri": "record(19)", }, Object { "author": Object { @@ -1106,30 +1176,30 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(11)", + "uri": "record(12)", "val": "self-label-a", }, Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(11)", + "uri": "record(12)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, - "cid": "cids(16)", + "cid": "cids(17)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [], @@ -1139,7 +1209,7 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "subject": "user(3)", }, - "uri": "record(10)", + "uri": "record(11)", }, Object { "author": Object { @@ -1153,7 +1223,7 @@ Array [ "muted": false, }, }, - "cid": "cids(17)", + "cid": "cids(18)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": true, "labels": Array [], @@ -1229,6 +1299,41 @@ Array [ "isRead": false, "labels": Array [], "reason": "repost", + "reasonSubject": "record(3)", + "record": Object { + "$type": "app.bsky.feed.repost", + "createdAt": "1970-01-01T00:00:00.000Z", + "subject": Object { + "cid": "cids(2)", + "uri": "record(3)", + }, + }, + "uri": "record(5)", + }, + Object { + "author": Object { + "did": "user(1)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(1)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(6)", + "muted": false, + }, + }, + "cid": "cids(4)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "isRead": false, + "labels": Array [], + "reason": "repost", "reasonSubject": "record(4)", "record": Object { "$type": "app.bsky.feed.repost", @@ -1238,7 +1343,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(5)", + "uri": "record(7)", }, Object { "author": Object { @@ -1252,7 +1357,7 @@ Array [ "muted": false, }, }, - "cid": "cids(4)", + "cid": "cids(5)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -1273,7 +1378,7 @@ Array [ }, "text": "of course", }, - "uri": "record(7)", + "uri": "record(8)", }, Object { "author": Object { @@ -1285,47 +1390,47 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(11)", + "uri": "record(12)", "val": "self-label-a", }, Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(11)", + "uri": "record(12)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [ Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(8)", + "uri": "record(9)", "val": "test-label", }, Object { - "cid": "cids(5)", + "cid": "cids(6)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(8)", + "uri": "record(9)", "val": "test-label-2", }, ], @@ -1343,7 +1448,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(7)", + "$link": "cids(8)", }, "size": 4114, }, @@ -1362,7 +1467,7 @@ Array [ }, "text": "hear that label_me label_me_2", }, - "uri": "record(8)", + "uri": "record(9)", }, Object { "author": Object { @@ -1383,7 +1488,7 @@ Array [ "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -1397,7 +1502,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(12)", + "uri": "record(13)", }, Object { "author": Object { @@ -1411,21 +1516,21 @@ Array [ "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "like", - "reasonSubject": "record(14)", + "reasonSubject": "record(15)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(10)", - "uri": "record(14)", + "cid": "cids(11)", + "uri": "record(15)", }, }, - "uri": "record(13)", + "uri": "record(14)", }, Object { "author": Object { @@ -1439,7 +1544,7 @@ Array [ "muted": false, }, }, - "cid": "cids(11)", + "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -1453,7 +1558,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(15)", + "uri": "record(16)", }, Object { "author": Object { @@ -1465,44 +1570,44 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(11)", + "uri": "record(12)", "val": "self-label-a", }, Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(11)", + "uri": "record(12)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, - "cid": "cids(12)", + "cid": "cids(13)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], "reason": "like", - "reasonSubject": "record(14)", + "reasonSubject": "record(15)", "record": Object { "$type": "app.bsky.feed.like", "createdAt": "1970-01-01T00:00:00.000Z", "subject": Object { - "cid": "cids(10)", - "uri": "record(14)", + "cid": "cids(11)", + "uri": "record(15)", }, }, - "uri": "record(16)", + "uri": "record(17)", }, Object { "author": Object { @@ -1514,30 +1619,30 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(11)", + "uri": "record(12)", "val": "self-label-a", }, Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(11)", + "uri": "record(12)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, - "cid": "cids(13)", + "cid": "cids(14)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -1551,7 +1656,7 @@ Array [ "uri": "record(4)", }, }, - "uri": "record(17)", + "uri": "record(18)", }, Object { "author": Object { @@ -1572,7 +1677,7 @@ Array [ "muted": false, }, }, - "cid": "cids(14)", + "cid": "cids(15)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -1583,8 +1688,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(15)", - "uri": "record(19)", + "cid": "cids(16)", + "uri": "record(20)", }, }, "facets": Array [ @@ -1603,7 +1708,7 @@ Array [ ], "text": "@alice.bluesky.xyz is the best", }, - "uri": "record(18)", + "uri": "record(19)", }, Object { "author": Object { @@ -1615,30 +1720,30 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(11)", + "uri": "record(12)", "val": "self-label-a", }, Object { - "cid": "cids(6)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(11)", + "uri": "record(12)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(10)", - "following": "record(9)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, - "cid": "cids(16)", + "cid": "cids(17)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], @@ -1648,7 +1753,7 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "subject": "user(3)", }, - "uri": "record(10)", + "uri": "record(11)", }, Object { "author": Object { @@ -1662,7 +1767,7 @@ Array [ "muted": false, }, }, - "cid": "cids(17)", + "cid": "cids(18)", "indexedAt": "1970-01-01T00:00:00.000Z", "isRead": false, "labels": Array [], diff --git a/packages/pds/tests/views/__snapshots__/posts.test.ts.snap b/packages/pds/tests/views/__snapshots__/posts.test.ts.snap index 311d2665430..75ff566186e 100644 --- a/packages/pds/tests/views/__snapshots__/posts.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/posts.test.ts.snap @@ -518,7 +518,7 @@ Array [ "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, + "repostCount": 1, "uri": "record(13)", "viewer": Object {}, }, diff --git a/packages/pds/tests/views/__snapshots__/thread.test.ts.snap b/packages/pds/tests/views/__snapshots__/thread.test.ts.snap index 195d40db206..df37cbb712d 100644 --- a/packages/pds/tests/views/__snapshots__/thread.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/thread.test.ts.snap @@ -59,7 +59,7 @@ Object { "text": "thanks bob", }, "replyCount": 0, - "repostCount": 1, + "repostCount": 2, "uri": "record(0)", "viewer": Object { "repost": "record(6)", @@ -128,7 +128,7 @@ Object { "text": "thanks bob", }, "replyCount": 0, - "repostCount": 1, + "repostCount": 2, "uri": "record(0)", "viewer": Object { "repost": "record(6)", @@ -341,7 +341,7 @@ Object { "text": "thanks bob", }, "replyCount": 0, - "repostCount": 1, + "repostCount": 2, "uri": "record(7)", "viewer": Object { "repost": "record(8)", @@ -715,7 +715,7 @@ Object { "text": "thanks bob", }, "replyCount": 0, - "repostCount": 1, + "repostCount": 2, "uri": "record(0)", "viewer": Object { "repost": "record(6)", @@ -967,7 +967,7 @@ Object { "text": "thanks bob", }, "replyCount": 0, - "repostCount": 1, + "repostCount": 2, "uri": "record(9)", "viewer": Object { "repost": "record(10)", @@ -1135,7 +1135,7 @@ Object { "text": "thanks bob", }, "replyCount": 0, - "repostCount": 1, + "repostCount": 2, "uri": "record(0)", "viewer": Object { "repost": "record(6)", @@ -1868,7 +1868,7 @@ Object { "text": "thanks bob", }, "replyCount": 0, - "repostCount": 1, + "repostCount": 2, "uri": "record(9)", "viewer": Object { "repost": "record(10)", diff --git a/packages/pds/tests/views/__snapshots__/timeline.test.ts.snap b/packages/pds/tests/views/__snapshots__/timeline.test.ts.snap index c200df7f35a..73f5e4f6748 100644 --- a/packages/pds/tests/views/__snapshots__/timeline.test.ts.snap +++ b/packages/pds/tests/views/__snapshots__/timeline.test.ts.snap @@ -35,6 +35,83 @@ Array [ "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(3)", + "uri": "record(3)", + }, + "root": Object { + "cid": "cids(2)", + "uri": "record(2)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object {}, + }, + "reason": Object { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": Object { + "did": "user(1)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(1)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(4)", + "muted": false, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], "likeCount": 3, "record": Object { "$type": "app.bsky.feed.post", @@ -43,7 +120,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, "reason": Object { @@ -62,7 +139,7 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -99,7 +176,7 @@ Array [ "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -109,18 +186,18 @@ Array [ "reply": Object { "parent": Object { "cid": "cids(3)", - "uri": "record(4)", + "uri": "record(3)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, - "uri": "record(3)", + "repostCount": 1, + "uri": "record(0)", "viewer": Object {}, }, }, @@ -173,7 +250,7 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -279,7 +356,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -290,7 +367,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -310,7 +387,7 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -374,7 +451,7 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -493,6 +570,83 @@ Array [ "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(3)", + "uri": "record(3)", + }, + "root": Object { + "cid": "cids(2)", + "uri": "record(2)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object {}, + }, + "reason": Object { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": Object { + "did": "user(1)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(1)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(4)", + "muted": false, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], "likeCount": 3, "record": Object { "$type": "app.bsky.feed.post", @@ -501,7 +655,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, "reason": Object { @@ -520,7 +674,7 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -557,7 +711,7 @@ Array [ "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -567,18 +721,18 @@ Array [ "reply": Object { "parent": Object { "cid": "cids(3)", - "uri": "record(4)", + "uri": "record(3)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, - "uri": "record(3)", + "repostCount": 1, + "uri": "record(0)", "viewer": Object {}, }, }, @@ -604,12 +758,12 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "of course", @@ -650,7 +804,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -661,7 +815,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, "root": Object { @@ -694,7 +848,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -705,7 +859,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -856,7 +1010,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -867,7 +1021,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -887,7 +1041,7 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -1217,13 +1371,23 @@ Array [ "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "likeCount": 3, + "likeCount": 0, "record": Object { "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000000Z", - "text": "again", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(3)", + "uri": "record(3)", + }, + "root": Object { + "cid": "cids(2)", + "uri": "record(2)", + }, + }, + "text": "thanks bob", }, - "replyCount": 2, + "replyCount": 0, "repostCount": 1, "uri": "record(0)", "viewer": Object {}, @@ -1244,57 +1408,269 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, "indexedAt": "1970-01-01T00:00:00.000Z", }, - }, - Object { - "post": Object { - "author": Object { - "did": "user(1)", - "handle": "dan.test", - "labels": Array [ - Object { - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "user(1)", - "val": "repo-action-label", - }, - ], - "viewer": Object { - "blockedBy": false, - "following": "record(2)", - "muted": false, - }, - }, - "cid": "cids(2)", - "embed": Object { - "$type": "app.bsky.embed.record#view", - "record": Object { - "$type": "app.bsky.embed.record#viewRecord", - "author": Object { - "did": "user(2)", - "handle": "carol.test", - "labels": Array [], - "viewer": Object { - "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", - "muted": false, - }, - }, - "cid": "cids(3)", - "embeds": Array [ - Object { - "$type": "app.bsky.embed.recordWithMedia#view", - "media": Object { - "$type": "app.bsky.embed.images#view", - "images": Array [ - Object { + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(2)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(6)", + "following": "record(5)", + "muted": false, + }, + }, + "cid": "cids(3)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(3)", + "val": "test-label", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(3)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(5)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(2)", + "uri": "record(2)", + }, + "root": Object { + "cid": "cids(2)", + "uri": "record(2)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(3)", + "viewer": Object {}, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(2)", + "viewer": Object {}, + }, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(2)", + "viewer": Object {}, + }, + "reason": Object { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": Object { + "did": "user(1)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(1)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(4)", + "muted": false, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(1)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(1)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(4)", + "muted": false, + }, + }, + "cid": "cids(6)", + "embed": Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "did": "user(3)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(11)", + "following": "record(10)", + "muted": false, + }, + }, + "cid": "cids(7)", + "embeds": Array [ + Object { + "$type": "app.bsky.embed.recordWithMedia#view", + "media": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { "alt": "tests/image/fixtures/key-landscape-small.jpg", "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", @@ -1311,47 +1687,47 @@ Array [ "$type": "app.bsky.embed.record#viewRecord", "author": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", + "did": "user(2)", "displayName": "bobby", "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-a", }, Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(7)", + "uri": "record(12)", "val": "kind", }, ], - "uri": "record(7)", + "uri": "record(12)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1368,15 +1744,15 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(9)", "val": "kind", }, ], - "uri": "record(4)", + "uri": "record(9)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -1391,7 +1767,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1402,7 +1778,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(8)", }, "size": 12736, }, @@ -1411,8 +1787,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(6)", - "uri": "record(7)", + "cid": "cids(9)", + "uri": "record(12)", }, }, }, @@ -1429,8 +1805,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(3)", - "uri": "record(4)", + "cid": "cids(7)", + "uri": "record(9)", }, }, "facets": Array [ @@ -1451,19 +1827,19 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(3)", + "uri": "record(8)", "viewer": Object {}, }, "reason": Object { "$type": "app.bsky.feed.defs#reasonRepost", "by": Object { - "did": "user(2)", + "did": "user(3)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, @@ -1500,7 +1876,7 @@ Array [ "muted": false, }, }, - "cid": "cids(8)", + "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -1509,19 +1885,19 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(9)", - "uri": "record(12)", + "cid": "cids(3)", + "uri": "record(3)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, - "uri": "record(11)", + "repostCount": 1, + "uri": "record(0)", "viewer": Object {}, }, "reply": Object { @@ -1529,35 +1905,35 @@ Array [ "$type": "app.bsky.feed.defs#postView", "author": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", + "did": "user(2)", "displayName": "bobby", "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-a", }, Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -1571,19 +1947,19 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(9)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(12)", + "uri": "record(3)", "val": "test-label", }, Object { - "cid": "cids(9)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(12)", + "uri": "record(3)", "val": "test-label-2", }, ], @@ -1600,7 +1976,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1609,19 +1985,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(12)", + "uri": "record(3)", "viewer": Object {}, }, "root": Object { @@ -1654,7 +2030,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1665,7 +2041,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -1673,13 +2049,13 @@ Array [ Object { "post": Object { "author": Object { - "did": "user(2)", + "did": "user(3)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, @@ -1692,12 +2068,12 @@ Array [ "createdAt": "1970-01-01T00:00:00.000Z", "reply": Object { "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "of course", @@ -1738,7 +2114,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1749,7 +2125,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, "root": Object { @@ -1782,7 +2158,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1793,7 +2169,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -1802,35 +2178,35 @@ Array [ "post": Object { "author": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", + "did": "user(2)", "displayName": "bobby", "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-a", }, Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -1844,19 +2220,19 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(9)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(12)", + "uri": "record(3)", "val": "test-label", }, Object { - "cid": "cids(9)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(12)", + "uri": "record(3)", "val": "test-label-2", }, ], @@ -1873,7 +2249,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -1882,19 +2258,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(12)", + "uri": "record(3)", "viewer": Object {}, }, "reply": Object { @@ -1928,7 +2304,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1939,7 +2315,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, "root": Object { @@ -1972,7 +2348,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -1983,7 +2359,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -2037,40 +2413,40 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(6)", "embeds": Array [ Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(2)", + "did": "user(3)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(9)", "val": "kind", }, ], - "uri": "record(4)", + "uri": "record(9)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2085,7 +2461,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -2096,7 +2472,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(8)", }, "size": 12736, }, @@ -2105,8 +2481,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(6)", - "uri": "record(7)", + "cid": "cids(9)", + "uri": "record(12)", }, }, }, @@ -2117,15 +2493,15 @@ Array [ ], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], - "uri": "record(3)", + "uri": "record(8)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(3)", - "uri": "record(4)", + "cid": "cids(7)", + "uri": "record(9)", }, }, "facets": Array [ @@ -2164,8 +2540,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(2)", - "uri": "record(3)", + "cid": "cids(6)", + "uri": "record(8)", }, }, "text": "yoohoo label_me", @@ -2180,31 +2556,31 @@ Array [ "post": Object { "author": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", + "did": "user(2)", "displayName": "bobby", "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-a", }, Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, @@ -2253,7 +2629,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -2264,7 +2640,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -2284,27 +2660,27 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(6)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "did": "user(2)", + "did": "user(3)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(7)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -2328,47 +2704,47 @@ Array [ "$type": "app.bsky.embed.record#viewRecord", "author": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", + "did": "user(2)", "displayName": "bobby", "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-a", }, Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(7)", + "uri": "record(12)", "val": "kind", }, ], - "uri": "record(7)", + "uri": "record(12)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2385,15 +2761,15 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(9)", "val": "kind", }, ], - "uri": "record(4)", + "uri": "record(9)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2408,7 +2784,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -2419,7 +2795,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(8)", }, "size": 12736, }, @@ -2428,8 +2804,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(6)", - "uri": "record(7)", + "cid": "cids(9)", + "uri": "record(12)", }, }, }, @@ -2446,8 +2822,8 @@ Array [ "embed": Object { "$type": "app.bsky.embed.record", "record": Object { - "cid": "cids(3)", - "uri": "record(4)", + "cid": "cids(7)", + "uri": "record(9)", }, }, "facets": Array [ @@ -2468,7 +2844,7 @@ Array [ }, "replyCount": 0, "repostCount": 1, - "uri": "record(3)", + "uri": "record(8)", "viewer": Object {}, }, }, @@ -2488,7 +2864,7 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -2510,17 +2886,17 @@ Array [ Object { "post": Object { "author": Object { - "did": "user(2)", + "did": "user(3)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(6)", - "following": "record(5)", + "followedBy": "record(11)", + "following": "record(10)", "muted": false, }, }, - "cid": "cids(3)", + "cid": "cids(7)", "embed": Object { "$type": "app.bsky.embed.recordWithMedia#view", "media": Object { @@ -2543,48 +2919,48 @@ Array [ "$type": "app.bsky.embed.record#viewRecord", "author": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", + "did": "user(2)", "displayName": "bobby", "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-a", }, Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(9)", "embeds": Array [], "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(7)", + "uri": "record(12)", "val": "kind", }, ], - "uri": "record(7)", + "uri": "record(12)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -2600,11 +2976,11 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(7)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(9)", "val": "kind", }, ], @@ -2623,7 +2999,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -2634,7 +3010,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(5)", + "$link": "cids(8)", }, "size": 12736, }, @@ -2643,8 +3019,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(6)", - "uri": "record(7)", + "cid": "cids(9)", + "uri": "record(12)", }, }, }, @@ -2652,7 +3028,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(4)", + "uri": "record(9)", "viewer": Object { "like": "record(17)", }, @@ -2662,43 +3038,43 @@ Array [ "post": Object { "author": Object { "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(3)", + "did": "user(2)", "displayName": "bobby", "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-a", }, Object { - "cid": "cids(7)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(10)", + "src": "user(2)", + "uri": "record(7)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(9)", - "following": "record(8)", + "followedBy": "record(6)", + "following": "record(5)", "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(6)", + "cid": "cids(9)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(7)", + "uri": "record(12)", "val": "kind", }, ], @@ -2714,7 +3090,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(7)", + "uri": "record(12)", "viewer": Object {}, }, }, @@ -3052,7 +3428,7 @@ Array [ "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, + "repostCount": 1, "uri": "record(6)", "viewer": Object {}, }, @@ -4362,7 +4738,7 @@ Array [ "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, + "repostCount": 1, "uri": "record(6)", "viewer": Object {}, }, @@ -5089,19 +5465,246 @@ Array [ }, ], }, - "text": "hey there", + "text": "hey there", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(16)", + "viewer": Object {}, + }, + }, +] +`; + +exports[`timeline views fetches authenticated user's home feed w/ reverse-chronological algorithm 4`] = ` +Array [ + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(1)", + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(3)", + "uri": "record(4)", + }, + "root": Object { + "cid": "cids(2)", + "uri": "record(3)", + }, + }, + "text": "thanks bob", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object { + "repost": "record(5)", + }, + }, + "reason": Object { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": Object { + "did": "user(1)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(1)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + }, + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(2)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(8)", + "muted": false, + }, + }, + "cid": "cids(3)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(4)", + "val": "test-label", + }, + Object { + "cid": "cids(3)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(4)", + "val": "test-label-2", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(5)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(2)", + "uri": "record(3)", + }, + "root": Object { + "cid": "cids(2)", + "uri": "record(3)", + }, + }, + "text": "hear that label_me label_me_2", + }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(4)", + "viewer": Object {}, + }, + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(1)", + "muted": false, + }, + }, + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(3)", + "viewer": Object { + "like": "record(7)", + "repost": "record(6)", + }, }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(16)", - "viewer": Object {}, }, }, -] -`; - -exports[`timeline views fetches authenticated user's home feed w/ reverse-chronological algorithm 4`] = ` -Array [ Object { "post": Object { "author": Object { @@ -5133,7 +5736,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -5144,10 +5747,10 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(3)", "viewer": Object { - "like": "record(4)", - "repost": "record(3)", + "like": "record(7)", + "repost": "record(6)", }, }, "reason": Object { @@ -5181,29 +5784,29 @@ Array [ "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(7)", + "uri": "record(9)", "val": "self-label-a", }, Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(7)", + "uri": "record(9)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "following": "record(6)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(3)", "embed": Object { "$type": "app.bsky.embed.images#view", "images": Array [ @@ -5217,19 +5820,19 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(5)", + "uri": "record(4)", "val": "test-label", }, Object { - "cid": "cids(2)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(5)", + "uri": "record(4)", "val": "test-label-2", }, ], @@ -5246,7 +5849,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -5255,19 +5858,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(3)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(3)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(5)", + "uri": "record(4)", "viewer": Object {}, }, "reply": Object { @@ -5302,7 +5905,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -5313,10 +5916,10 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(3)", "viewer": Object { - "like": "record(4)", - "repost": "record(3)", + "like": "record(7)", + "repost": "record(6)", }, }, "root": Object { @@ -5350,7 +5953,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -5361,10 +5964,10 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(3)", "viewer": Object { - "like": "record(4)", - "repost": "record(3)", + "like": "record(7)", + "repost": "record(6)", }, }, }, @@ -5378,29 +5981,29 @@ Array [ "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(7)", + "uri": "record(9)", "val": "self-label-a", }, Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(7)", + "uri": "record(9)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "following": "record(6)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(5)", + "cid": "cids(6)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -5411,7 +6014,7 @@ Array [ }, "replyCount": 0, "repostCount": 0, - "uri": "record(8)", + "uri": "record(10)", "viewer": Object {}, }, }, @@ -5434,7 +6037,7 @@ Array [ "muted": false, }, }, - "cid": "cids(6)", + "cid": "cids(7)", "embed": Object { "$type": "app.bsky.embed.record#view", "record": Object { @@ -5448,7 +6051,7 @@ Array [ "muted": false, }, }, - "cid": "cids(7)", + "cid": "cids(8)", "embeds": Array [ Object { "$type": "app.bsky.embed.recordWithMedia#view", @@ -5477,41 +6080,41 @@ Array [ "handle": "bob.test", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(7)", + "uri": "record(9)", "val": "self-label-a", }, Object { - "cid": "cids(3)", + "cid": "cids(4)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(7)", + "uri": "record(9)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "following": "record(6)", + "following": "record(8)", "muted": false, }, }, - "cid": "cids(9)", + "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(9)", + "cid": "cids(10)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(11)", + "uri": "record(13)", "val": "kind", }, ], - "uri": "record(11)", + "uri": "record(13)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -5528,15 +6131,15 @@ Array [ "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(7)", + "cid": "cids(8)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(10)", + "uri": "record(12)", "val": "kind", }, ], - "uri": "record(10)", + "uri": "record(12)", "value": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", @@ -5551,7 +6154,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(4)", + "$link": "cids(5)", }, "size": 4114, }, @@ -5562,7 +6165,7 @@ Array [ "$type": "blob", "mimeType": "image/jpeg", "ref": Object { - "$link": "cids(8)", + "$link": "cids(9)", }, "size": 12736, }, @@ -5571,8 +6174,8 @@ Array [ }, "record": Object { "record": Object { - "cid": "cids(9)", - "uri": "record(11)", + "cid": "cids(10)", + "uri": "record(13)", }, }, }, @@ -5586,38 +6189,190 @@ Array [ "record": Object { "$type": "app.bsky.feed.post", "createdAt": "1970-01-01T00:00:00.000Z", - "embed": Object { - "$type": "app.bsky.embed.record", - "record": Object { - "cid": "cids(7)", - "uri": "record(10)", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(8)", + "uri": "record(12)", + }, + }, + "facets": Array [ + Object { + "features": Array [ + Object { + "$type": "app.bsky.richtext.facet#mention", + "did": "user(0)", + }, + ], + "index": Object { + "byteEnd": 18, + "byteStart": 0, + }, + }, + ], + "text": "@alice.bluesky.xyz is the best", + }, + "replyCount": 0, + "repostCount": 1, + "uri": "record(11)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "did": "user(1)", + "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(1)", + "val": "repo-action-label", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(11)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "text": "dan here!", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(14)", + "viewer": Object {}, + }, + }, + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(2)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(9)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(9)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "following": "record(8)", + "muted": false, + }, + }, + "cid": "cids(10)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(10)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(13)", + "val": "kind", + }, + ], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "langs": Array [ + "en-US", + "i-klingon", + ], + "text": "bob back at it again!", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(13)", + "viewer": Object {}, + }, + }, +] +`; + +exports[`timeline views omits posts and reposts of muted authors. 1`] = ` +Array [ + Object { + "post": Object { + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(3)", + "uri": "record(3)", }, - }, - "facets": Array [ - Object { - "features": Array [ - Object { - "$type": "app.bsky.richtext.facet#mention", - "did": "user(0)", - }, - ], - "index": Object { - "byteEnd": 18, - "byteStart": 0, - }, + "root": Object { + "cid": "cids(2)", + "uri": "record(2)", }, - ], - "text": "@alice.bluesky.xyz is the best", + }, + "text": "thanks bob", }, "replyCount": 0, "repostCount": 1, - "uri": "record(9)", + "uri": "record(0)", "viewer": Object {}, }, - }, - Object { - "post": Object { - "author": Object { + "reason": Object { + "$type": "app.bsky.feed.defs#reasonRepost", + "by": Object { "did": "user(1)", "handle": "dan.test", "labels": Array [ @@ -5631,88 +6386,158 @@ Array [ ], "viewer": Object { "blockedBy": false, + "following": "record(4)", "muted": false, }, }, - "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "text": "dan here!", - }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(12)", - "viewer": Object {}, }, - }, - Object { - "post": Object { - "author": Object { - "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", - "did": "user(2)", - "displayName": "bobby", - "handle": "bob.test", + "reply": Object { + "parent": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(2)", + "displayName": "bobby", + "handle": "bob.test", + "labels": Array [ + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-a", + }, + Object { + "cid": "cids(4)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(2)", + "uri": "record(7)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(6)", + "following": "record(5)", + "muted": true, + }, + }, + "cid": "cids(3)", + "embed": Object { + "$type": "app.bsky.embed.images#view", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + "thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg", + }, + ], + }, + "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-a", + "src": "did:example:labeler", + "uri": "record(3)", + "val": "test-label", }, Object { "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(2)", - "uri": "record(7)", - "val": "self-label-b", + "src": "did:example:labeler", + "uri": "record(3)", + "val": "test-label-2", }, ], - "viewer": Object { - "blockedBy": false, - "following": "record(6)", - "muted": false, + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.images", + "images": Array [ + Object { + "alt": "tests/image/fixtures/key-landscape-small.jpg", + "image": Object { + "$type": "blob", + "mimeType": "image/jpeg", + "ref": Object { + "$link": "cids(5)", + }, + "size": 4114, + }, + }, + ], + }, + "reply": Object { + "parent": Object { + "cid": "cids(2)", + "uri": "record(2)", + }, + "root": Object { + "cid": "cids(2)", + "uri": "record(2)", + }, + }, + "text": "hear that label_me label_me_2", }, + "replyCount": 1, + "repostCount": 0, + "uri": "record(3)", + "viewer": Object {}, }, - "cid": "cids(9)", - "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [ - Object { - "cid": "cids(9)", - "cts": "1970-01-01T00:00:00.000Z", - "neg": false, - "src": "did:example:labeler", - "uri": "record(11)", - "val": "kind", + "root": Object { + "$type": "app.bsky.feed.defs#postView", + "author": Object { + "avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, }, - ], - "likeCount": 0, - "record": Object { - "$type": "app.bsky.feed.post", - "createdAt": "1970-01-01T00:00:00.000Z", - "langs": Array [ - "en-US", - "i-klingon", - ], - "text": "bob back at it again!", + "cid": "cids(2)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(2)", + "viewer": Object {}, }, - "replyCount": 0, - "repostCount": 0, - "uri": "record(11)", - "viewer": Object {}, }, }, -] -`; - -exports[`timeline views omits posts and reposts of muted authors. 1`] = ` -Array [ Object { "post": Object { "author": Object { @@ -5743,7 +6568,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -5754,7 +6579,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, "reason": Object { @@ -5773,7 +6598,7 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -5810,7 +6635,7 @@ Array [ "muted": false, }, }, - "cid": "cids(2)", + "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 0, @@ -5820,18 +6645,18 @@ Array [ "reply": Object { "parent": Object { "cid": "cids(3)", - "uri": "record(4)", + "uri": "record(3)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "thanks bob", }, "replyCount": 0, - "repostCount": 0, - "uri": "record(3)", + "repostCount": 1, + "uri": "record(0)", "viewer": Object {}, }, "reply": Object { @@ -5885,7 +6710,7 @@ Array [ "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(3)", "val": "test-label", }, Object { @@ -5893,7 +6718,7 @@ Array [ "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(4)", + "uri": "record(3)", "val": "test-label-2", }, ], @@ -5919,19 +6744,19 @@ Array [ }, "reply": Object { "parent": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, "root": Object { - "cid": "cids(0)", - "uri": "record(0)", + "cid": "cids(2)", + "uri": "record(2)", }, }, "text": "hear that label_me label_me_2", }, "replyCount": 1, "repostCount": 0, - "uri": "record(4)", + "uri": "record(3)", "viewer": Object {}, }, "root": Object { @@ -5964,7 +6789,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -5975,7 +6800,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -6029,7 +6854,7 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -6198,7 +7023,7 @@ Array [ "muted": false, }, }, - "cid": "cids(0)", + "cid": "cids(2)", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "likeCount": 3, @@ -6209,7 +7034,7 @@ Array [ }, "replyCount": 2, "repostCount": 1, - "uri": "record(0)", + "uri": "record(2)", "viewer": Object {}, }, }, @@ -6229,7 +7054,7 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, @@ -6433,7 +7258,7 @@ Array [ ], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": false, }, }, diff --git a/packages/pds/tests/views/notifications.test.ts b/packages/pds/tests/views/notifications.test.ts index b5d9907140c..1cc86455126 100644 --- a/packages/pds/tests/views/notifications.test.ts +++ b/packages/pds/tests/views/notifications.test.ts @@ -56,7 +56,7 @@ describe('pds notification views', () => { { headers: sc.getHeaders(alice) }, ) - expect(notifCountAlice.data.count).toBe(11) + expect(notifCountAlice.data.count).toBe(12) const notifCountBob = await agent.api.app.bsky.notification.getUnreadCount( {}, @@ -83,7 +83,7 @@ describe('pds notification views', () => { { headers: sc.getHeaders(alice) }, ) - expect(notifCountAlice.data.count).toBe(12) + expect(notifCountAlice.data.count).toBe(13) const notifCountBob = await agent.api.app.bsky.notification.getUnreadCount( {}, @@ -129,7 +129,7 @@ describe('pds notification views', () => { ) const notifs = sort(notifRes.data.notifications) - expect(notifs.length).toBe(12) + expect(notifs.length).toBe(13) const readStates = notifs.map((notif) => notif.isRead) expect(readStates).toEqual(notifs.map(() => false)) @@ -209,9 +209,9 @@ describe('pds notification views', () => { ) const notifs = sort(notifRes.data.notifications) - expect(notifs.length).toBe(10) + expect(notifs.length).toBe(11) expect(forSnapshot(notifs)).toMatchSnapshot() - expect(notifCount.data.count).toBe(10) + expect(notifCount.data.count).toBe(11) // Cleanup await Promise.all( @@ -257,7 +257,7 @@ describe('pds notification views', () => { }, ) - expect(full.data.notifications.length).toEqual(12) + expect(full.data.notifications.length).toEqual(13) expect(results(paginatedAll)).toEqual(results([full.data])) }) @@ -300,7 +300,7 @@ describe('pds notification views', () => { ) const notifs = sort(notifRes.data.notifications) - expect(notifs.length).toBe(12) + expect(notifs.length).toBe(13) const readStates = notifs.map((notif) => notif.isRead) expect(readStates).toEqual(notifs.map((_, i) => i >= 3)) From 33685b84081e9d8f6d0f79d6ba3d3813b98a6c94 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 17 Aug 2023 15:50:12 -0500 Subject: [PATCH 157/237] format --- packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 0cef444881a..6a0a37584f2 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -59,12 +59,11 @@ export default function (server: Server, ctx: AppContext) { .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'), ) } else if (filter === 'posts_no_replies') { - feedItemsQb = feedItemsQb - .where((qb) => { - return qb - .where('post.replyParent', 'is', null) - .orWhere('type', '=', 'repost') - }) + feedItemsQb = feedItemsQb.where((qb) => { + return qb + .where('post.replyParent', 'is', null) + .orWhere('type', '=', 'repost') + }) } if (viewer !== null) { From 88ea0b269336d9f8d4aed62662bc3aeb4c0a0685 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 17 Aug 2023 17:16:21 -0500 Subject: [PATCH 158/237] update test --- .../__snapshots__/notifications.test.ts.snap | 16 ++++++++-------- packages/bsky/tests/views/notifications.test.ts | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap b/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap index 71c16d7420f..5fddc479c76 100644 --- a/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/notifications.test.ts.snap @@ -69,7 +69,7 @@ Array [ }, "cid": "cids(4)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, + "isRead": false, "labels": Array [], "reason": "like", "reasonSubject": "record(4)", @@ -97,7 +97,7 @@ Array [ }, "cid": "cids(5)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, + "isRead": false, "labels": Array [], "reason": "follow", "record": Object { @@ -156,7 +156,7 @@ Array [ }, "cid": "cids(7)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, + "isRead": false, "labels": Array [], "reason": "like", "reasonSubject": "record(10)", @@ -184,7 +184,7 @@ Array [ }, "cid": "cids(9)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, + "isRead": false, "labels": Array [], "reason": "like", "reasonSubject": "record(4)", @@ -216,7 +216,7 @@ Array [ }, "cid": "cids(10)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, + "isRead": false, "labels": Array [], "reason": "follow", "record": Object { @@ -244,7 +244,7 @@ Array [ }, "cid": "cids(12)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, + "isRead": false, "labels": Array [ Object { "cid": "cids(12)", @@ -316,7 +316,7 @@ Array [ }, "cid": "cids(14)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, + "isRead": false, "labels": Array [], "reason": "like", "reasonSubject": "record(10)", @@ -348,7 +348,7 @@ Array [ }, "cid": "cids(15)", "indexedAt": "1970-01-01T00:00:00.000Z", - "isRead": true, + "isRead": false, "labels": Array [], "reason": "like", "reasonSubject": "record(4)", diff --git a/packages/bsky/tests/views/notifications.test.ts b/packages/bsky/tests/views/notifications.test.ts index 4f83815d782..b125ffc3570 100644 --- a/packages/bsky/tests/views/notifications.test.ts +++ b/packages/bsky/tests/views/notifications.test.ts @@ -56,7 +56,7 @@ describe('notification views', () => { { headers: await network.serviceHeaders(alice) }, ) - expect(notifCountAlice.data.count).toBe(13) + expect(notifCountAlice.data.count).toBe(12) const notifCountBob = await agent.api.app.bsky.notification.getUnreadCount( {}, @@ -218,7 +218,7 @@ describe('notification views', () => { ) const notifs = notifRes.data.notifications - expect(notifs.length).toBe(12) + expect(notifs.length).toBe(13) const readStates = notifs.map((notif) => notif.isRead) expect(readStates).toEqual(notifs.map((n) => n.indexedAt <= seenAt)) From 1c50428428347b65fffed60ff76b08bb502030a7 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 17 Aug 2023 18:21:13 -0500 Subject: [PATCH 159/237] tweak author-feed rejects test syntax --- packages/bsky/tests/views/author-feed.test.ts | 11 +++++------ packages/pds/tests/views/author-feed.test.ts | 11 +++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index 8e5fcc509f1..c33b45b9338 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -165,12 +165,11 @@ describe('pds author feed views', () => { }, ) - expect(async () => { - await agent.api.app.bsky.feed.getAuthorFeed( - { actor: alice }, - { headers: await network.serviceHeaders(carol) }, - ) - }).rejects.toThrow('Profile not found') + const attempt = agent.api.app.bsky.feed.getAuthorFeed( + { actor: alice }, + { headers: await network.serviceHeaders(carol) }, + ) + expect(attempt).rejects.toThrow('Profile not found') // Cleanup await agent.api.com.atproto.admin.reverseModerationAction( diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index 94f63257cfc..c51123fc9a1 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -197,12 +197,11 @@ describe('pds author feed views', () => { did: alice, }) - expect(async () => { - await agent.api.app.bsky.feed.getAuthorFeed( - { actor: alice }, - { headers: sc.getHeaders(carol) }, - ) - }).rejects.toThrow('Profile not found') + const attempt = agent.api.app.bsky.feed.getAuthorFeed( + { actor: alice }, + { headers: sc.getHeaders(carol) }, + ) + expect(attempt).rejects.toThrow('Profile not found') // Cleanup await reverseModerationAction(action.id) From 2413f48c934028639d77ede26dff3b090034853d Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 17 Aug 2023 18:24:42 -0500 Subject: [PATCH 160/237] Increase CI test matrix size (#1490) * increase test matrix size * tweak author feed tests --- .github/workflows/repo.yaml | 4 ++-- packages/bsky/tests/views/author-feed.test.ts | 11 +++++------ packages/pds/tests/views/author-feed.test.ts | 12 +++++------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/workflows/repo.yaml b/.github/workflows/repo.yaml index 80903117c4c..f04ab7b6914 100644 --- a/.github/workflows/repo.yaml +++ b/.github/workflows/repo.yaml @@ -21,7 +21,7 @@ jobs: test: strategy: matrix: - shard: [1/4, 2/4, 3/4, 4/4] + shard: [1/8, 2/8, 3/8, 4/8, 5/8, 6/8, 7/8, 8/8] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -30,7 +30,7 @@ jobs: node-version: 18 cache: "yarn" - run: yarn install --frozen-lockfile - - run: yarn test:withFlags --maxWorkers=2 --shard=${{ matrix.shard }} --passWithNoTests + - run: yarn test:withFlags --maxWorkers=1 --shard=${{ matrix.shard }} --passWithNoTests verify: runs-on: ubuntu-latest steps: diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index 8e6c7b20ce7..30032bd2bb4 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -165,12 +165,11 @@ describe('pds author feed views', () => { }, ) - expect(async () => { - await agent.api.app.bsky.feed.getAuthorFeed( - { actor: alice }, - { headers: await network.serviceHeaders(carol) }, - ) - }).rejects.toThrow('Profile not found') + const attempt = agent.api.app.bsky.feed.getAuthorFeed( + { actor: alice }, + { headers: await network.serviceHeaders(carol) }, + ) + expect(attempt).rejects.toThrow('Profile not found') // Cleanup await agent.api.com.atproto.admin.reverseModerationAction( diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index 3e7ee8be947..47766fe7ac6 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -12,7 +12,6 @@ import basicSeed from '../seeds/basic' import { isRecord } from '../../src/lexicon/types/app/bsky/feed/post' import { isView as isEmbedRecordWithMedia } from '../../src/lexicon/types/app/bsky/embed/recordWithMedia' import { isView as isImageEmbed } from '../../src/lexicon/types/app/bsky/embed/images' -import { InvalidRequestError } from '@atproto/xrpc-server' describe('pds author feed views', () => { let agent: AtpAgent @@ -197,12 +196,11 @@ describe('pds author feed views', () => { did: alice, }) - expect(async () => { - await agent.api.app.bsky.feed.getAuthorFeed( - { actor: alice }, - { headers: sc.getHeaders(carol) }, - ) - }).rejects.toThrow('Profile not found') + const attempt = agent.api.app.bsky.feed.getAuthorFeed( + { actor: alice }, + { headers: sc.getHeaders(carol) }, + ) + expect(attempt).rejects.toThrow('Profile not found') // Cleanup await reverseModerationAction(action.id) From 2b228a1e6f3a9ad04168fa4e92daa767c02d836a Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 17 Aug 2023 18:30:29 -0500 Subject: [PATCH 161/237] styleguide --- packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts | 8 ++++---- .../pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 6a0a37584f2..62301c21cb4 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -59,11 +59,11 @@ export default function (server: Server, ctx: AppContext) { .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'), ) } else if (filter === 'posts_no_replies') { - feedItemsQb = feedItemsQb.where((qb) => { - return qb + feedItemsQb = feedItemsQb.where((qb) => + qb .where('post.replyParent', 'is', null) - .orWhere('type', '=', 'repost') - }) + .orWhere('type', '=', 'repost'), + ) } if (viewer !== null) { diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index cece01aa2d9..fe8a36d15f4 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -65,11 +65,11 @@ export default function (server: Server, ctx: AppContext) { } else if (params.filter === 'posts_no_replies') { feedItemsQb = feedItemsQb // only posts, no replies - .where((qb) => { - return qb + .where((qb) => + qb .where('post.replyParent', 'is', null) - .orWhere('type', '=', 'repost') - }) + .orWhere('type', '=', 'repost'), + ) } // for access-based auth, enforce blocks and mutes From 8de64178c07b07ffcdaf25ae5afa78831168d02f Mon Sep 17 00:00:00 2001 From: devin ivy Date: Fri, 18 Aug 2023 13:32:54 -0400 Subject: [PATCH 162/237] Runtime flags in PDS, appview-proxy flags (#1491) * basic system for runtime flags in pds * apply appview-proxy runtime flags, tidy/fixes * fix no-did case --- packages/pds/package.json | 1 + .../api/com/atproto/identity/resolveHandle.ts | 2 +- .../pds/src/api/com/atproto/repo/getRecord.ts | 2 +- .../app-view/api/app/bsky/actor/getProfile.ts | 2 +- .../api/app/bsky/actor/getProfiles.ts | 2 +- .../api/app/bsky/actor/getSuggestions.ts | 2 +- .../api/app/bsky/actor/searchActors.ts | 2 +- .../app/bsky/actor/searchActorsTypeahead.ts | 2 +- .../api/app/bsky/feed/getActorFeeds.ts | 2 +- .../api/app/bsky/feed/getAuthorFeed.ts | 2 +- .../src/app-view/api/app/bsky/feed/getFeed.ts | 2 +- .../api/app/bsky/feed/getFeedGenerator.ts | 2 +- .../api/app/bsky/feed/getFeedGenerators.ts | 2 +- .../app-view/api/app/bsky/feed/getLikes.ts | 2 +- .../api/app/bsky/feed/getPostThread.ts | 2 +- .../app-view/api/app/bsky/feed/getPosts.ts | 2 +- .../api/app/bsky/feed/getRepostedBy.ts | 2 +- .../app-view/api/app/bsky/feed/getTimeline.ts | 2 +- .../app-view/api/app/bsky/graph/getBlocks.ts | 2 +- .../api/app/bsky/graph/getFollowers.ts | 2 +- .../app-view/api/app/bsky/graph/getFollows.ts | 2 +- .../app-view/api/app/bsky/graph/getList.ts | 2 +- .../api/app/bsky/graph/getListMutes.ts | 2 +- .../app-view/api/app/bsky/graph/getLists.ts | 2 +- .../app-view/api/app/bsky/graph/getMutes.ts | 2 +- .../app/bsky/notification/getUnreadCount.ts | 2 +- .../bsky/notification/listNotifications.ts | 2 +- .../src/app-view/api/app/bsky/unspecced.ts | 2 +- packages/pds/src/context.ts | 30 +++++-- packages/pds/src/db/database-schema.ts | 2 + .../20230818T134357818Z-runtime-flags.ts | 13 +++ packages/pds/src/db/migrations/index.ts | 1 + packages/pds/src/db/tables/runtime-flag.ts | 8 ++ packages/pds/src/index.ts | 6 ++ packages/pds/src/runtime-flags.ts | 86 +++++++++++++++++++ yarn.lock | 5 ++ 36 files changed, 173 insertions(+), 33 deletions(-) create mode 100644 packages/pds/src/db/migrations/20230818T134357818Z-runtime-flags.ts create mode 100644 packages/pds/src/db/tables/runtime-flag.ts create mode 100644 packages/pds/src/runtime-flags.ts diff --git a/packages/pds/package.json b/packages/pds/package.json index b27ce107f4c..59b37c5de87 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -56,6 +56,7 @@ "http-terminator": "^3.2.0", "jsonwebtoken": "^8.5.1", "kysely": "^0.22.0", + "lru-cache": "^10.0.1", "multiformats": "^9.6.4", "nodemailer": "^6.8.0", "nodemailer-html-to-text": "^3.2.0", diff --git a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts index bfa47b47764..7bcf9498b6f 100644 --- a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts @@ -34,7 +34,7 @@ export default function (server: Server, ctx: AppContext) { // this is not someone on our server, but we help with resolving anyway - if (!did && ctx.canProxyRead(req)) { + if (!did && (await ctx.canProxyRead(req))) { did = await tryResolveFromAppview(ctx.appviewAgent, handle) } diff --git a/packages/pds/src/api/com/atproto/repo/getRecord.ts b/packages/pds/src/api/com/atproto/repo/getRecord.ts index bec4fff1e8c..99f5c8e18c2 100644 --- a/packages/pds/src/api/com/atproto/repo/getRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/getRecord.ts @@ -26,7 +26,7 @@ export default function (server: Server, ctx: AppContext) { } } - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req)) { const res = await ctx.appviewAgent.api.com.atproto.repo.getRecord(params) return { encoding: 'application/json', diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts b/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts index 1826b583f54..bb4678c36ad 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getProfile.ts @@ -13,7 +13,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, auth, params }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.actor.getProfile( params, requester diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts b/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts index e17edce430d..966f0b9d10b 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getProfiles.ts @@ -9,7 +9,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.actor.getProfiles( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts b/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts index edd426fe9e7..4d58bf4986c 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/getSuggestions.ts @@ -7,7 +7,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.actor.getSuggestions( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts b/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts index 20516204254..7e22848498f 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/searchActors.ts @@ -16,7 +16,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.actor.searchActors( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts index 9a133c59ccb..cc35c5d2a7d 100644 --- a/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/pds/src/app-view/api/app/bsky/actor/searchActorsTypeahead.ts @@ -14,7 +14,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.actor.searchActorsTypeahead( params, diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts b/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts index 4ddbe49188c..253b31886da 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getActorFeeds.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getActorFeeds( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index fe8a36d15f4..bc559e1e894 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -15,7 +15,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getAuthorFeed( params, requester diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts index 8620f83b730..50652c6c65c 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts @@ -30,7 +30,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const { data: feed } = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( { feed: params.feed }, diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts index 7f4de6fa327..b87a2d32e65 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerator.ts @@ -12,7 +12,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerator( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts index 0e504abd39d..ed3be795b28 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeedGenerators.ts @@ -6,7 +6,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getFeedGenerators( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts b/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts index 63d82d4d53d..ed2a950d866 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getLikes.ts @@ -9,7 +9,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getLikes( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts index 009eb4bafed..7a39f4f4889 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts @@ -47,7 +47,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { try { const res = await ctx.appviewAgent.api.app.bsky.feed.getPostThread( params, diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts index 48e764862c0..c4265d9c1cd 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPosts.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getPosts( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts b/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts index fb243809a79..7b713e8a8a9 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getRepostedBy.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getRepostedBy( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts b/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts index 350165c1387..26778c4b4a4 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getTimeline.ts @@ -19,7 +19,7 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`) } - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getTimeline( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts b/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts index b3eaef40af5..100331d0226 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getBlocks.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.graph.getBlocks( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts b/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts index 1e0c0779cce..b14d11c3ac6 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getFollowers.ts @@ -11,7 +11,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.graph.getFollowers( params, requester diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts b/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts index 59863835aa9..82e9acee29f 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getFollows.ts @@ -11,7 +11,7 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ req, params, auth }) => { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.graph.getFollows( params, requester diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getList.ts b/packages/pds/src/app-view/api/app/bsky/graph/getList.ts index 00eeaba88a5..e3a925a2101 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getList.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getList.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.graph.getList( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts b/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts index 21a64437ae0..b3c7905f32a 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getListMutes.ts @@ -7,7 +7,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.graph.getListMutes( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts b/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts index 58b83a20db2..ac27522ead7 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getLists.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.graph.getLists( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts b/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts index c26b7b1db25..d8578916811 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/getMutes.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.graph.getMutes( params, await ctx.serviceAuthHeaders(requester), diff --git a/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts b/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts index c1b7c276d9f..a199f42a299 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/pds/src/app-view/api/app/bsky/notification/getUnreadCount.ts @@ -8,7 +8,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, auth, params }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.notification.getUnreadCount( params, diff --git a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts b/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts index 0e17234f7ab..bf688dc25ed 100644 --- a/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts +++ b/packages/pds/src/app-view/api/app/bsky/notification/listNotifications.ts @@ -12,7 +12,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.notification.listNotifications( params, diff --git a/packages/pds/src/app-view/api/app/bsky/unspecced.ts b/packages/pds/src/app-view/api/app/bsky/unspecced.ts index 3a281fb4a14..e50758dbb7a 100644 --- a/packages/pds/src/app-view/api/app/bsky/unspecced.ts +++ b/packages/pds/src/app-view/api/app/bsky/unspecced.ts @@ -27,7 +27,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.accessVerifier, handler: async ({ req, params, auth }) => { const requester = auth.credentials.did - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const hotClassicUri = Object.keys(ctx.algos).find((uri) => uri.endsWith('/hot-classic'), ) diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 74161be1711..25697ff75b6 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -21,6 +21,7 @@ import { MountedAlgos } from './feed-gen/types' import { Crawlers } from './crawlers' import { LabelCache } from './label-cache' import { ContentReporter } from './content-reporter' +import { RuntimeFlags } from './runtime-flags' export class AppContext { constructor( @@ -42,6 +43,7 @@ export class AppContext { sequencerLeader: SequencerLeader | null labeler: Labeler labelCache: LabelCache + runtimeFlags: RuntimeFlags contentReporter?: ContentReporter backgroundQueue: BackgroundQueue appviewAgent?: AtpAgent @@ -138,6 +140,10 @@ export class AppContext { return this.opts.labelCache } + get runtimeFlags(): RuntimeFlags { + return this.opts.runtimeFlags + } + get contentReporter(): ContentReporter | undefined { return this.opts.contentReporter } @@ -185,12 +191,24 @@ export class AppContext { return this.opts.appviewAgent } - canProxyRead(req: express.Request): boolean { - return ( - this.cfg.bskyAppViewProxy && - this.cfg.bskyAppViewEndpoint !== undefined && - req.get('x-appview-proxy') !== undefined - ) + async canProxyRead( + req: express.Request, + did?: string | null, + ): Promise { + if (!this.cfg.bskyAppViewProxy || !this.cfg.bskyAppViewEndpoint) { + return false + } + if (req.get('x-appview-proxy') !== undefined) { + return true + } + // e.g. /xrpc/a.b.c.d/ -> a.b.c.d/ -> a.b.c.d + const endpoint = req.path.replace('/xrpc/', '').replaceAll('/', '') + if (!did) { + // when no did assigned, only proxy reads if threshold is at max of 10 + const threshold = this.runtimeFlags.appviewProxy.getThreshold(endpoint) + return threshold === 10 + } + return await this.runtimeFlags.appviewProxy.shouldProxy(endpoint, did) } canProxyFeedConstruction(req: express.Request): boolean { diff --git a/packages/pds/src/db/database-schema.ts b/packages/pds/src/db/database-schema.ts index 03ff0e2eca4..47b09e97211 100644 --- a/packages/pds/src/db/database-schema.ts +++ b/packages/pds/src/db/database-schema.ts @@ -23,9 +23,11 @@ import * as listMute from './tables/list-mute' import * as label from './tables/label' import * as repoSeq from './tables/repo-seq' import * as appMigration from './tables/app-migration' +import * as runtimeFlag from './tables/runtime-flag' import * as appView from '../app-view/db' export type DatabaseSchemaType = appView.DatabaseSchemaType & + runtimeFlag.PartialDB & appMigration.PartialDB & userAccount.PartialDB & userState.PartialDB & diff --git a/packages/pds/src/db/migrations/20230818T134357818Z-runtime-flags.ts b/packages/pds/src/db/migrations/20230818T134357818Z-runtime-flags.ts new file mode 100644 index 00000000000..c93ccd74158 --- /dev/null +++ b/packages/pds/src/db/migrations/20230818T134357818Z-runtime-flags.ts @@ -0,0 +1,13 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('runtime_flag') + .addColumn('name', 'varchar', (col) => col.primaryKey()) + .addColumn('value', 'varchar', (col) => col.notNull()) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('runtime_flag').execute() +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index 96f74e2d82c..43b92b8d911 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -61,3 +61,4 @@ export * as _20230801T195109532Z from './20230801T195109532Z-remove-moderation-f export * as _20230807T035309811Z from './20230807T035309811Z-feed-item-delete-invite-for-user-idx' export * as _20230808T172813122Z from './20230808T172813122Z-repo-rev' export * as _20230810T203412859Z from './20230810T203412859Z-action-duration' +export * as _20230818T134357818Z from './20230818T134357818Z-runtime-flags' diff --git a/packages/pds/src/db/tables/runtime-flag.ts b/packages/pds/src/db/tables/runtime-flag.ts new file mode 100644 index 00000000000..f1e701a6914 --- /dev/null +++ b/packages/pds/src/db/tables/runtime-flag.ts @@ -0,0 +1,8 @@ +export interface RuntimeFlag { + name: string + value: string +} + +export const tableName = 'runtime_flag' + +export type PartialDB = { [tableName]: RuntimeFlag } diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 45f9c8277d6..df927f0e282 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -46,6 +46,7 @@ import { Crawlers } from './crawlers' import { LabelCache } from './label-cache' import { ContentReporter } from './content-reporter' import { ModerationService } from './services/moderation' +import { RuntimeFlags } from './runtime-flags' export type { MountedAlgos } from './feed-gen/types' export type { ServerConfigValues } from './config' @@ -227,6 +228,8 @@ export class PDS { crawlers, }) + const runtimeFlags = new RuntimeFlags(db) + const ctx = new AppContext({ db, blobstore, @@ -241,6 +244,7 @@ export class PDS { sequencerLeader, labeler, labelCache, + runtimeFlags, contentReporter, services, mailer, @@ -309,6 +313,7 @@ export class PDS { await this.ctx.sequencer.start() await this.ctx.db.startListeningToChannels() this.ctx.labelCache.start() + await this.ctx.runtimeFlags.start() const server = this.app.listen(this.ctx.cfg.port) this.server = server this.server.keepAliveTimeout = 90000 @@ -318,6 +323,7 @@ export class PDS { } async destroy(): Promise { + this.ctx.runtimeFlags.destroy() this.ctx.labelCache.stop() await this.ctx.sequencerLeader?.destroy() await this.terminator?.terminate() diff --git a/packages/pds/src/runtime-flags.ts b/packages/pds/src/runtime-flags.ts new file mode 100644 index 00000000000..8349dd12b65 --- /dev/null +++ b/packages/pds/src/runtime-flags.ts @@ -0,0 +1,86 @@ +import { wait } from '@atproto/common' +import { randomIntFromSeed } from '@atproto/crypto' +import { LRUCache } from 'lru-cache' +import Database from './db' +import { dbLogger as log } from './logger' + +type AppviewProxyFlagName = `appview-proxy:${string}` + +export type FlagName = AppviewProxyFlagName + +export class RuntimeFlags { + destroyed = false + private flags = new Map() + public appviewProxy = new AppviewProxyFlags(this) + + constructor(public db: Database) {} + + async start() { + await this.refresh() + this.poll() + } + + destroy() { + this.destroyed = true + } + + get(flag: FlagName) { + return this.flags.get(flag) || null + } + + async refresh() { + const flags = await this.db.db + .selectFrom('runtime_flag') + .selectAll() + .execute() + this.flags = new Map() + for (const flag of flags) { + this.flags.set(flag.name, flag.value) + } + } + + async poll() { + try { + if (this.destroyed) return + await this.refresh() + } catch (err) { + log.error({ err }, 'runtime flags failed to refresh') + } + await wait(5000) + this.poll() + } +} + +class AppviewProxyFlags { + private partitionCache = new LRUCache({ + max: 50000, + fetchMethod(did: string) { + return randomIntFromSeed(did, 10) + }, + }) + + constructor(private runtimeFlags: RuntimeFlags) {} + + getThreshold(endpoint: string) { + const val = this.runtimeFlags.get(`appview-proxy:${endpoint}`) || '0' + const threshold = parseInt(val, 10) + return appviewFlagIsValid(threshold) ? threshold : 0 + } + + async shouldProxy(endpoint: string, did: string) { + const threshold = this.getThreshold(endpoint) + if (threshold === 0) { + return false + } + if (threshold === 10) { + return true + } + // threshold is 0 to 10 inclusive, partitions are 0 to 9 inclusive. + const partition = await this.partitionCache.fetch(did) + return partition !== undefined && partition < threshold + } +} + +const appviewFlagIsValid = (val: number) => { + return 0 <= val && val <= 10 +} diff --git a/yarn.lock b/yarn.lock index b419b3adca4..0eec71190f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8935,6 +8935,11 @@ lodash@^4.17.15, lodash@^4.17.19, lodash@^4.7.0: resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lru-cache@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a" + integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" From 9b1f5f01243cb37387b9fea435c2f7d6ef90e381 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 18 Aug 2023 16:25:07 -0500 Subject: [PATCH 163/237] only allow viewing your own likes --- .../src/api/app/bsky/feed/getActorLikes.ts | 48 ++----- packages/bsky/tests/views/actor-likes.test.ts | 120 ++++++----------- .../api/app/bsky/feed/getActorLikes.ts | 44 ++----- packages/pds/tests/views/actor-likes.test.ts | 124 ++++++------------ 4 files changed, 104 insertions(+), 232 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts index aa1652ff0ac..6ab91a22ef4 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts @@ -11,47 +11,28 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params, auth, res }) => { const { actor, limit, cursor } = params const viewer = auth.credentials.did - const db = ctx.db.db - const { ref } = db.dynamic + const db = ctx.db.getReplica() + const { ref } = db.db.dynamic - // first verify there is not a block between requester & subject - if (viewer !== null) { - const blocks = await ctx.services.graph(ctx.db).getBlocks(viewer, actor) - if (blocks.blocking) { - throw new InvalidRequestError( - `Requester has blocked actor: ${actor}`, - 'BlockedActor', - ) - } else if (blocks.blockedBy) { - throw new InvalidRequestError( - `Requester is blocked by actor: $${actor}`, - 'BlockedByActor', - ) - } - } + const actorService = ctx.services.actor(db) + const feedService = ctx.services.feed(db) + const graphService = ctx.services.graph(db) - const actorService = ctx.services.actor(ctx.db) - const feedService = ctx.services.feed(ctx.db) - const graphService = ctx.services.graph(ctx.db) + // maybe resolve did first + const actorRes = await actorService.getActor(actor) + if (!actorRes) { + throw new InvalidRequestError('Profile not found') + } + const actorDid = actorRes.did - let did = '' - if (actor.startsWith('did:')) { - did = actor - } else { - const actorRes = await db - .selectFrom('actor') - .select('did') - .where('handle', '=', actor) - .executeTakeFirst() - if (actorRes) { - did = actorRes?.did - } + if (!viewer || viewer !== actorDid) { + throw new InvalidRequestError('Likes are private') } let feedItemsQb = feedService .selectFeedItemQb() .innerJoin('like', 'like.subject', 'feed_item.uri') - .where('like.creator', '=', did) + .where('like.creator', '=', actorDid) if (viewer !== null) { feedItemsQb = feedItemsQb @@ -60,7 +41,6 @@ export default function (server: Server, ctx: AppContext) { graphService.whereNotMuted(qb, viewer, [ref('post.creator')]), ), ) - // TODO do we want this? was missing here .whereNotExists(graphService.blockQb(viewer, [ref('post.creator')])) } diff --git a/packages/bsky/tests/views/actor-likes.test.ts b/packages/bsky/tests/views/actor-likes.test.ts index f942ab7ca27..67310c83e30 100644 --- a/packages/bsky/tests/views/actor-likes.test.ts +++ b/packages/bsky/tests/views/actor-likes.test.ts @@ -48,66 +48,18 @@ describe('bsky actor likes feed views', () => { expect(bobLikes).toHaveLength(3) - const { - data: { feed: carolLikes }, - } = await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[carol].handle }, - { headers: await network.serviceHeaders(carol) }, - ) - - expect(carolLikes).toHaveLength(2) - - const { - data: { feed: aliceLikes }, - } = await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[alice].handle }, - { headers: await network.serviceHeaders(alice) }, - ) - - expect(aliceLikes).toHaveLength(1) - - const { - data: { feed: danLikes }, - } = await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[dan].handle }, - { headers: await network.serviceHeaders(dan) }, - ) - - expect(danLikes).toHaveLength(1) + await expect( + agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[bob].handle }, + { headers: await network.serviceHeaders(carol) }, + ), + ).rejects.toThrow('Likes are private') }) - it('actor blocks viewer', async () => { - const aliceBlockBob = await pdsAgent.api.app.bsky.graph.block.create( + it('viewer has blocked author of liked post(s)', async () => { + const bobBlocksAlice = await pdsAgent.api.app.bsky.graph.block.create( { - repo: alice, // alice blocks bob - }, - { - subject: bob, - createdAt: new Date().toISOString(), - }, - sc.getHeaders(alice), - ) - - try { - await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[alice].handle }, - { headers: await network.serviceHeaders(bob) }, - ) - } catch (e) { - expect(e).toBeInstanceOf(BlockedByActorError) - } - - // unblock - await pdsAgent.api.app.bsky.graph.block.delete( - { repo: alice, rkey: new AtUri(aliceBlockBob.uri).rkey }, - sc.getHeaders(alice), - ) - }) - - it('viewer has blocked actor', async () => { - const bobBlockAlice = await pdsAgent.api.app.bsky.graph.block.create( - { - repo: bob, // alice blocks bob + repo: bob, // bob blocks alice }, { subject: alice, @@ -116,61 +68,67 @@ describe('bsky actor likes feed views', () => { sc.getHeaders(bob), ) - try { - await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[alice].handle }, - { headers: await network.serviceHeaders(bob) }, - ) - } catch (e) { - expect(e).toBeInstanceOf(BlockedActorError) - } + const { + data: { feed }, + } = await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[bob].handle }, + { headers: await network.serviceHeaders(bob) }, + ) + + expect( + feed.every((item) => { + return item.post.author.did !== alice + }), + ).toBe(true) // unblock await pdsAgent.api.app.bsky.graph.block.delete( - { repo: bob, rkey: new AtUri(bobBlockAlice.uri).rkey }, + { repo: bob, rkey: new AtUri(bobBlocksAlice.uri).rkey }, sc.getHeaders(bob), ) }) - it('liked post(s) author(s) blocks viewer', async () => { - const aliceBlockDan = await pdsAgent.api.app.bsky.graph.block.create( + it('liked post author has blocked viewer', async () => { + const aliceBlockBob = await pdsAgent.api.app.bsky.graph.block.create( { - repo: alice, // alice blocks dan + repo: alice, // alice blocks bob }, { - subject: dan, + subject: bob, createdAt: new Date().toISOString(), }, sc.getHeaders(alice), ) - const { data } = await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[bob].handle }, // bob has liked alice's posts - { headers: await network.serviceHeaders(dan) }, + const { + data: { feed }, + } = await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[bob].handle }, + { headers: await network.serviceHeaders(bob) }, ) expect( - data.feed.every((item) => { + feed.every((item) => { return item.post.author.did !== alice }), - ).toBe(true) // alice's posts are filtered out + ).toBe(true) // unblock await pdsAgent.api.app.bsky.graph.block.delete( - { repo: alice, rkey: new AtUri(aliceBlockDan.uri).rkey }, + { repo: alice, rkey: new AtUri(aliceBlockBob.uri).rkey }, sc.getHeaders(alice), ) }) - it('liked post(s) author(s) muted by viewer', async () => { + it('viewer has muted author of liked post(s)', async () => { await pdsAgent.api.app.bsky.graph.muteActor( - { actor: alice }, // dan mutes alice - { headers: sc.getHeaders(dan), encoding: 'application/json' }, + { actor: alice }, // bob mutes alice + { headers: sc.getHeaders(bob), encoding: 'application/json' }, ) const { data } = await agent.api.app.bsky.feed.getActorLikes( { actor: sc.accounts[bob].handle }, // bob has liked alice's posts - { headers: await network.serviceHeaders(dan) }, + { headers: await network.serviceHeaders(bob) }, ) expect( @@ -181,7 +139,7 @@ describe('bsky actor likes feed views', () => { await pdsAgent.api.app.bsky.graph.unmuteActor( { actor: alice }, // dan unmutes alice - { headers: sc.getHeaders(dan), encoding: 'application/json' }, + { headers: sc.getHeaders(bob), encoding: 'application/json' }, ) }) }) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts index 56204deb1ad..ff7f8e128f9 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts @@ -36,28 +36,29 @@ export default function (server: Server, ctx: AppContext) { const { ref } = ctx.db.db.dynamic const accountService = ctx.services.account(ctx.db) + const actorService = ctx.services.appView.actor(ctx.db) const feedService = ctx.services.appView.feed(ctx.db) const graphService = ctx.services.appView.graph(ctx.db) - // resolve did - const userLookupCol = actor.startsWith('did:') - ? 'did_handle.did' - : 'did_handle.handle' - const actorDidQb = ctx.db.db - .selectFrom('did_handle') - .select('did') - .where(userLookupCol, '=', actor) - .limit(1) + // maybe resolve did first + const actorRes = await actorService.getActor(actor) + if (!actorRes) { + throw new InvalidRequestError('Profile not found') + } + const actorDid = actorRes.did + + if (!requester || requester !== actorDid) { + throw new InvalidRequestError('Likes are private') + } // defaults to posts, reposts, and replies let feedItemsQb = feedService .selectFeedItemQb() .innerJoin('like', 'like.subject', 'feed_item.uri') - .where('like.creator', '=', actorDidQb) + .where('like.creator', '=', actorDid) // for access-based auth, enforce blocks and mutes if (requester) { - await assertNoBlocks(ctx, { requester, actor }) feedItemsQb = feedItemsQb .where((qb) => qb.where((qb) => @@ -98,27 +99,6 @@ export default function (server: Server, ctx: AppContext) { }) } -// throws when there's a block between the two users -async function assertNoBlocks( - ctx: AppContext, - opts: { requester: string; actor: string }, -) { - const { requester, actor } = opts - const graphService = ctx.services.appView.graph(ctx.db) - const blocks = await graphService.getBlocks(requester, actor) - if (blocks.blocking) { - throw new InvalidRequestError( - `Requester has blocked actor: ${actor}`, - 'BlockedActor', - ) - } else if (blocks.blockedBy) { - throw new InvalidRequestError( - `Requester is blocked by actor: $${actor}`, - 'BlockedByActor', - ) - } -} - const getAuthorMunge = async ( ctx: AppContext, original: OutputSchema, diff --git a/packages/pds/tests/views/actor-likes.test.ts b/packages/pds/tests/views/actor-likes.test.ts index cd0db5e4518..bcf92b59cfd 100644 --- a/packages/pds/tests/views/actor-likes.test.ts +++ b/packages/pds/tests/views/actor-likes.test.ts @@ -2,10 +2,6 @@ import AtpAgent, { AtUri } from '@atproto/api' import { runTestServer, CloseFn } from '../_util' import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' -import { - BlockedByActorError, - BlockedActorError, -} from '@atproto/api/src/client/types/app/bsky/feed/getActorLikes' describe('pds actor likes feed views', () => { let agent: AtpAgent @@ -47,66 +43,18 @@ describe('pds actor likes feed views', () => { expect(bobLikes).toHaveLength(3) - const { - data: { feed: carolLikes }, - } = await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[carol].handle }, - { headers: sc.getHeaders(carol) }, - ) - - expect(carolLikes).toHaveLength(2) - - const { - data: { feed: aliceLikes }, - } = await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[alice].handle }, - { headers: sc.getHeaders(alice) }, - ) - - expect(aliceLikes).toHaveLength(1) - - const { - data: { feed: danLikes }, - } = await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[dan].handle }, - { headers: sc.getHeaders(dan) }, - ) - - expect(danLikes).toHaveLength(1) + await expect( + agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[bob].handle }, + { headers: sc.getHeaders(carol) }, + ), + ).rejects.toThrow('Likes are private') }) - it('actor blocks viewer', async () => { - const aliceBlockBob = await agent.api.app.bsky.graph.block.create( + it('viewer has blocked author of liked post(s)', async () => { + const bobBlocksAlice = await agent.api.app.bsky.graph.block.create( { - repo: alice, // alice blocks bob - }, - { - subject: bob, - createdAt: new Date().toISOString(), - }, - sc.getHeaders(alice), - ) - - try { - await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[alice].handle }, - { headers: sc.getHeaders(bob) }, - ) - } catch (e) { - expect(e).toBeInstanceOf(BlockedByActorError) - } - - // unblock - await agent.api.app.bsky.graph.block.delete( - { repo: alice, rkey: new AtUri(aliceBlockBob.uri).rkey }, - sc.getHeaders(alice), - ) - }) - - it('viewer has blocked actor', async () => { - const bobBlockAlice = await agent.api.app.bsky.graph.block.create( - { - repo: bob, // alice blocks bob + repo: bob, // bob blocks alice }, { subject: alice, @@ -115,61 +63,67 @@ describe('pds actor likes feed views', () => { sc.getHeaders(bob), ) - try { - await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[alice].handle }, - { headers: sc.getHeaders(bob) }, - ) - } catch (e) { - expect(e).toBeInstanceOf(BlockedActorError) - } + const { + data: { feed }, + } = await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[bob].handle }, + { headers: sc.getHeaders(bob) }, + ) + + expect( + feed.every((item) => { + return item.post.author.did !== alice + }), + ).toBe(true) // unblock await agent.api.app.bsky.graph.block.delete( - { repo: bob, rkey: new AtUri(bobBlockAlice.uri).rkey }, + { repo: bob, rkey: new AtUri(bobBlocksAlice.uri).rkey }, sc.getHeaders(bob), ) }) - it('liked post(s) author(s) blocks viewer', async () => { - const aliceBlockDan = await agent.api.app.bsky.graph.block.create( + it('liked post author has blocked viewer', async () => { + const aliceBlocksBob = await agent.api.app.bsky.graph.block.create( { - repo: alice, // alice blocks dan + repo: alice, // alice blocks bob }, { - subject: dan, + subject: bob, createdAt: new Date().toISOString(), }, sc.getHeaders(alice), ) - const { data } = await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[bob].handle }, // bob has liked alice's posts - { headers: sc.getHeaders(dan) }, + const { + data: { feed }, + } = await agent.api.app.bsky.feed.getActorLikes( + { actor: sc.accounts[bob].handle }, + { headers: sc.getHeaders(bob) }, ) expect( - data.feed.every((item) => { + feed.every((item) => { return item.post.author.did !== alice }), - ).toBe(true) // alice's posts are filtered out + ).toBe(true) // unblock await agent.api.app.bsky.graph.block.delete( - { repo: alice, rkey: new AtUri(aliceBlockDan.uri).rkey }, + { repo: alice, rkey: new AtUri(aliceBlocksBob.uri).rkey }, sc.getHeaders(alice), ) }) - it('liked post(s) author(s) muted by viewer', async () => { + it('viewer has muted author of liked post(s)', async () => { await agent.api.app.bsky.graph.muteActor( - { actor: alice }, // dan mutes alice - { headers: sc.getHeaders(dan), encoding: 'application/json' }, + { actor: alice }, // bob mutes alice + { headers: sc.getHeaders(bob), encoding: 'application/json' }, ) const { data } = await agent.api.app.bsky.feed.getActorLikes( { actor: sc.accounts[bob].handle }, // bob has liked alice's posts - { headers: sc.getHeaders(dan) }, + { headers: sc.getHeaders(bob) }, ) expect( @@ -180,7 +134,7 @@ describe('pds actor likes feed views', () => { await agent.api.app.bsky.graph.unmuteActor( { actor: alice }, // dan unmutes alice - { headers: sc.getHeaders(dan), encoding: 'application/json' }, + { headers: sc.getHeaders(bob), encoding: 'application/json' }, ) }) }) From cf1cd0610ad92f8245332d81e4cde859e5a9ae33 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 18 Aug 2023 17:20:40 -0500 Subject: [PATCH 164/237] fix canProxyRead updated usage --- packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts index ff7f8e128f9..9b2257dba78 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts @@ -16,7 +16,7 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.type === 'access' ? auth.credentials.did : null - if (ctx.canProxyRead(req)) { + if (await ctx.canProxyRead(req, requester)) { const res = await ctx.appviewAgent.api.app.bsky.feed.getActorLikes( params, requester From 3576d108454465825f691821cd6e38036578278f Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 21 Aug 2023 09:59:47 -0500 Subject: [PATCH 165/237] allow network to process --- packages/bsky/tests/views/actor-likes.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/bsky/tests/views/actor-likes.test.ts b/packages/bsky/tests/views/actor-likes.test.ts index 67310c83e30..602a8a45288 100644 --- a/packages/bsky/tests/views/actor-likes.test.ts +++ b/packages/bsky/tests/views/actor-likes.test.ts @@ -68,6 +68,8 @@ describe('bsky actor likes feed views', () => { sc.getHeaders(bob), ) + await network.processAll() + const { data: { feed }, } = await agent.api.app.bsky.feed.getActorLikes( @@ -100,6 +102,8 @@ describe('bsky actor likes feed views', () => { sc.getHeaders(alice), ) + await network.processAll() + const { data: { feed }, } = await agent.api.app.bsky.feed.getActorLikes( @@ -126,6 +130,8 @@ describe('bsky actor likes feed views', () => { { headers: sc.getHeaders(bob), encoding: 'application/json' }, ) + await network.processAll() + const { data } = await agent.api.app.bsky.feed.getActorLikes( { actor: sc.accounts[bob].handle }, // bob has liked alice's posts { headers: await network.serviceHeaders(bob) }, From be794d2026e1e2e068b271717ff644b5f49f417f Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 21 Aug 2023 10:02:15 -0500 Subject: [PATCH 166/237] opaque errors --- packages/bsky/src/api/app/bsky/feed/getActorLikes.ts | 2 +- packages/bsky/tests/views/actor-likes.test.ts | 2 +- packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts | 2 +- packages/pds/tests/views/actor-likes.test.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts index 6ab91a22ef4..3a757ed31cb 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts @@ -26,7 +26,7 @@ export default function (server: Server, ctx: AppContext) { const actorDid = actorRes.did if (!viewer || viewer !== actorDid) { - throw new InvalidRequestError('Likes are private') + throw new InvalidRequestError('Profile not found') } let feedItemsQb = feedService diff --git a/packages/bsky/tests/views/actor-likes.test.ts b/packages/bsky/tests/views/actor-likes.test.ts index 602a8a45288..8dab3aa7d62 100644 --- a/packages/bsky/tests/views/actor-likes.test.ts +++ b/packages/bsky/tests/views/actor-likes.test.ts @@ -53,7 +53,7 @@ describe('bsky actor likes feed views', () => { { actor: sc.accounts[bob].handle }, { headers: await network.serviceHeaders(carol) }, ), - ).rejects.toThrow('Likes are private') + ).rejects.toThrow('Profile not found') }) it('viewer has blocked author of liked post(s)', async () => { diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts index 9b2257dba78..4fdaffc768e 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts @@ -48,7 +48,7 @@ export default function (server: Server, ctx: AppContext) { const actorDid = actorRes.did if (!requester || requester !== actorDid) { - throw new InvalidRequestError('Likes are private') + throw new InvalidRequestError('Profile not found') } // defaults to posts, reposts, and replies diff --git a/packages/pds/tests/views/actor-likes.test.ts b/packages/pds/tests/views/actor-likes.test.ts index bcf92b59cfd..966aabf6197 100644 --- a/packages/pds/tests/views/actor-likes.test.ts +++ b/packages/pds/tests/views/actor-likes.test.ts @@ -48,7 +48,7 @@ describe('pds actor likes feed views', () => { { actor: sc.accounts[bob].handle }, { headers: sc.getHeaders(carol) }, ), - ).rejects.toThrow('Likes are private') + ).rejects.toThrow('Profile not found') }) it('viewer has blocked author of liked post(s)', async () => { From b45c9aab6ba33828c425fa66285d005f55fc08ad Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 21 Aug 2023 14:30:34 -0500 Subject: [PATCH 167/237] Misc fixes (#1492) * proxy typeahead * proxy post thread * disable migration * error handling on db * fix case where replica explicitly configured w/ no tags * appview build * tidy * block handling on some appview endpoints --------- Co-authored-by: Devin Ivy --- .../src/api/app/bsky/feed/getAuthorFeed.ts | 18 ++++++++++-------- .../bsky/src/api/app/bsky/feed/getLikes.ts | 3 +++ .../src/api/app/bsky/feed/getRepostedBy.ts | 4 ++++ packages/bsky/src/db/coordinator.ts | 2 +- packages/pds/service/index.js | 1 - packages/pds/src/db/index.ts | 5 +++++ .../pds/src/event-stream/background-queue.ts | 2 +- 7 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 62301c21cb4..b5dac940cf1 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -67,14 +67,16 @@ export default function (server: Server, ctx: AppContext) { } if (viewer !== null) { - feedItemsQb = feedItemsQb.where((qb) => - // Hide reposts of muted content - qb - .where('type', '=', 'post') - .orWhere((qb) => - graphService.whereNotMuted(qb, viewer, [ref('post.creator')]), - ), - ) + feedItemsQb = feedItemsQb + .where((qb) => + // Hide reposts of muted content + qb + .where('type', '=', 'post') + .orWhere((qb) => + graphService.whereNotMuted(qb, viewer, [ref('post.creator')]), + ), + ) + .whereNotExists(graphService.blockQb(viewer, [ref('post.creator')])) } const keyset = new FeedKeyset( diff --git a/packages/bsky/src/api/app/bsky/feed/getLikes.ts b/packages/bsky/src/api/app/bsky/feed/getLikes.ts index 7a46beab9c2..7ecd3b4e2db 100644 --- a/packages/bsky/src/api/app/bsky/feed/getLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getLikes.ts @@ -12,6 +12,8 @@ export default function (server: Server, ctx: AppContext) { const requester = auth.credentials.did const db = ctx.db.getReplica() + const graphService = ctx.services.graph(db) + const { ref } = db.db.dynamic let builder = db.db @@ -19,6 +21,7 @@ export default function (server: Server, ctx: AppContext) { .where('like.subject', '=', uri) .innerJoin('actor as creator', 'creator.did', 'like.creator') .where(notSoftDeletedClause(ref('creator'))) + .whereNotExists(graphService.blockQb(requester, [ref('like.creator')])) .selectAll('creator') .select([ 'like.cid as cid', diff --git a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts index 9671dd14c70..69ad1e5c79e 100644 --- a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts @@ -10,6 +10,7 @@ export default function (server: Server, ctx: AppContext) { const { uri, limit, cursor, cid } = params const requester = auth.credentials.did const db = ctx.db.getReplica() + const graphService = ctx.services.graph(db) const { ref } = db.db.dynamic let builder = db.db @@ -17,6 +18,9 @@ export default function (server: Server, ctx: AppContext) { .where('repost.subject', '=', uri) .innerJoin('actor as creator', 'creator.did', 'repost.creator') .where(notSoftDeletedClause(ref('creator'))) + .whereNotExists( + graphService.blockQb(requester, [ref('repost.creator')]), + ) .selectAll('creator') .select(['repost.cid as cid', 'repost.sortAt as sortAt']) diff --git a/packages/bsky/src/db/coordinator.ts b/packages/bsky/src/db/coordinator.ts index 64933b8e2fe..a8f4cc3016c 100644 --- a/packages/bsky/src/db/coordinator.ts +++ b/packages/bsky/src/db/coordinator.ts @@ -46,7 +46,7 @@ export class DatabaseCoordinator { }) this.allReplicas.push(db) // setup different groups of replicas based on tag, each round-robins separately. - if (cfg.tags) { + if (cfg.tags?.length) { for (const tag of cfg.tags) { if (tag === '*') { this.untagged.dbs.push(db) diff --git a/packages/pds/service/index.js b/packages/pds/service/index.js index 7b94acc9b91..5c4d8e924a7 100644 --- a/packages/pds/service/index.js +++ b/packages/pds/service/index.js @@ -37,7 +37,6 @@ const main = async () => { // view-maintainer lock then one for anything else. poolSize: 2, }) - await migrateDb.migrateToLatestOrThrow() // Use lower-credentialed user to run the app const db = Database.postgres({ url: pgUrl(env.dbCreds), diff --git a/packages/pds/src/db/index.ts b/packages/pds/src/db/index.ts index b98706e7bd9..8b1880df722 100644 --- a/packages/pds/src/db/index.ts +++ b/packages/pds/src/db/index.ts @@ -79,7 +79,9 @@ export class Database { 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) { @@ -382,3 +384,6 @@ class LeakyTxPlugin implements KyselyPlugin { type TxLockRes = { rows: { acquired: true | false }[] } + +const onPoolError = (err: Error) => log.error({ err }, 'db pool error') +const onClientError = (err: Error) => log.error({ err }, 'db client error') diff --git a/packages/pds/src/event-stream/background-queue.ts b/packages/pds/src/event-stream/background-queue.ts index d23c8a49a38..aa7671ccc07 100644 --- a/packages/pds/src/event-stream/background-queue.ts +++ b/packages/pds/src/event-stream/background-queue.ts @@ -5,7 +5,7 @@ import { dbLogger } from '../logger' // A simple queue for in-process, out-of-band/backgrounded work export class BackgroundQueue { - queue = new PQueue({ concurrency: 10 }) + queue = new PQueue({ concurrency: 5 }) destroyed = false constructor(public db: Database) {} From 24a5414b64e923239c5307da843deca3889960bc Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 21 Aug 2023 15:42:45 -0500 Subject: [PATCH 168/237] make sure to await rejected promise expectations --- packages/bsky/tests/views/author-feed.test.ts | 2 +- packages/pds/tests/views/author-feed.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bsky/tests/views/author-feed.test.ts b/packages/bsky/tests/views/author-feed.test.ts index c33b45b9338..62e0fd0826e 100644 --- a/packages/bsky/tests/views/author-feed.test.ts +++ b/packages/bsky/tests/views/author-feed.test.ts @@ -169,7 +169,7 @@ describe('pds author feed views', () => { { actor: alice }, { headers: await network.serviceHeaders(carol) }, ) - expect(attempt).rejects.toThrow('Profile not found') + await expect(attempt).rejects.toThrow('Profile not found') // Cleanup await agent.api.com.atproto.admin.reverseModerationAction( diff --git a/packages/pds/tests/views/author-feed.test.ts b/packages/pds/tests/views/author-feed.test.ts index acbbe7e8eee..2f9e8603b7b 100644 --- a/packages/pds/tests/views/author-feed.test.ts +++ b/packages/pds/tests/views/author-feed.test.ts @@ -200,7 +200,7 @@ describe('pds author feed views', () => { { actor: alice }, { headers: sc.getHeaders(carol) }, ) - expect(attempt).rejects.toThrow('Profile not found') + await expect(attempt).rejects.toThrow('Profile not found') // Cleanup await reverseModerationAction(action.id) From 1d663df5038aa31a567058689217110f83743483 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 21 Aug 2023 15:56:58 -0500 Subject: [PATCH 169/237] Fix runtime flags open handles (#1497) fix runtime flags open handles --- packages/common-web/src/util.ts | 9 ++++++--- packages/pds/src/index.ts | 2 +- packages/pds/src/runtime-flags.ts | 10 +++++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/common-web/src/util.ts b/packages/common-web/src/util.ts index 5777a83f961..04fd227af55 100644 --- a/packages/common-web/src/util.ts +++ b/packages/common-web/src/util.ts @@ -17,9 +17,12 @@ export const wait = (ms: number) => { return new Promise((res) => setTimeout(res, ms)) } -export const bailableWait = ( - ms: number, -): { bail: () => void; wait: () => Promise } => { +export type BailableWait = { + bail: () => void + wait: () => Promise +} + +export const bailableWait = (ms: number): BailableWait => { let bail const waitPromise = new Promise((res) => { const timeout = setTimeout(res, ms) diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index df927f0e282..5409795f731 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -323,7 +323,7 @@ export class PDS { } async destroy(): Promise { - this.ctx.runtimeFlags.destroy() + await this.ctx.runtimeFlags.destroy() this.ctx.labelCache.stop() await this.ctx.sequencerLeader?.destroy() await this.terminator?.terminate() diff --git a/packages/pds/src/runtime-flags.ts b/packages/pds/src/runtime-flags.ts index 8349dd12b65..052ed4973f6 100644 --- a/packages/pds/src/runtime-flags.ts +++ b/packages/pds/src/runtime-flags.ts @@ -1,4 +1,4 @@ -import { wait } from '@atproto/common' +import { BailableWait, bailableWait } from '@atproto/common' import { randomIntFromSeed } from '@atproto/crypto' import { LRUCache } from 'lru-cache' import Database from './db' @@ -11,6 +11,7 @@ export type FlagName = AppviewProxyFlagName export class RuntimeFlags { destroyed = false private flags = new Map() + private pollWait: BailableWait | undefined = undefined public appviewProxy = new AppviewProxyFlags(this) constructor(public db: Database) {} @@ -20,8 +21,10 @@ export class RuntimeFlags { this.poll() } - destroy() { + async destroy() { this.destroyed = true + this.pollWait?.bail() + await this.pollWait?.wait() } get(flag: FlagName) { @@ -46,7 +49,8 @@ export class RuntimeFlags { } catch (err) { log.error({ err }, 'runtime flags failed to refresh') } - await wait(5000) + this.pollWait = bailableWait(5000) + await this.pollWait.wait() this.poll() } } From 5316d3f65d2701afa37c94688b6b26290e243e32 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 21 Aug 2023 15:41:49 -0500 Subject: [PATCH 170/237] do not notify users of own-actions --- .../src/services/indexing/plugins/like.ts | 4 +- .../src/services/indexing/plugins/repost.ts | 4 +- packages/bsky/tests/indexing.test.ts | 93 ++++++++++++ .../services/indexing/plugins/like.ts | 4 +- .../services/indexing/plugins/repost.ts | 4 +- packages/pds/tests/indexing.test.ts | 137 +++++++++++++++++- 6 files changed, 241 insertions(+), 5 deletions(-) diff --git a/packages/bsky/src/services/indexing/plugins/like.ts b/packages/bsky/src/services/indexing/plugins/like.ts index 34ea4e0f6ad..5bbe9521ada 100644 --- a/packages/bsky/src/services/indexing/plugins/like.ts +++ b/packages/bsky/src/services/indexing/plugins/like.ts @@ -53,7 +53,9 @@ const findDuplicate = async ( const notifsForInsert = (obj: IndexedLike) => { const subjectUri = new AtUri(obj.subject) - return [ + // prevent self-notifications + const isSelf = subjectUri.host === obj.creator + return isSelf ? [] : [ { did: subjectUri.host, author: obj.creator, diff --git a/packages/bsky/src/services/indexing/plugins/repost.ts b/packages/bsky/src/services/indexing/plugins/repost.ts index 6e806804dda..3813bba698d 100644 --- a/packages/bsky/src/services/indexing/plugins/repost.ts +++ b/packages/bsky/src/services/indexing/plugins/repost.ts @@ -72,7 +72,9 @@ const findDuplicate = async ( const notifsForInsert = (obj: IndexedRepost) => { const subjectUri = new AtUri(obj.subject) - return [ + // prevent self-notifications + const isSelf = subjectUri.host === obj.creator + return isSelf ? [] : [ { did: subjectUri.host, author: obj.creator, diff --git a/packages/bsky/tests/indexing.test.ts b/packages/bsky/tests/indexing.test.ts index 14dd7e65ef4..4ca4f729f8b 100644 --- a/packages/bsky/tests/indexing.test.ts +++ b/packages/bsky/tests/indexing.test.ts @@ -267,6 +267,99 @@ describe('indexing', () => { await services.indexing(db).deleteRecord(...del(originalPost[0])) }) + it('does not notify user of own like or repost', async () => { + const { db, services } = network.bsky.indexer.ctx + const createdAt = new Date().toISOString() + + const originalPost = await prepareCreate({ + did: sc.dids.bob, + collection: ids.AppBskyFeedPost, + record: { + $type: ids.AppBskyFeedPost, + text: 'original post', + createdAt, + } as AppBskyFeedPost.Record, + }) + + const originalPostRef = { + uri: originalPost[0].toString(), + cid: originalPost[1].toString(), + } + + // own actions + const ownLike = await prepareCreate({ + did: sc.dids.bob, + collection: ids.AppBskyFeedLike, + record: { + $type: ids.AppBskyFeedLike, + subject: originalPostRef, + createdAt, + } as AppBskyFeedLike.Record, + }) + const ownRepost = await prepareCreate({ + did: sc.dids.bob, + collection: ids.AppBskyFeedRepost, + record: { + $type: ids.AppBskyFeedRepost, + subject: originalPostRef, + createdAt, + } as AppBskyFeedRepost.Record, + }) + + // other actions + const aliceLike = await prepareCreate({ + did: sc.dids.alice, + collection: ids.AppBskyFeedLike, + record: { + $type: ids.AppBskyFeedLike, + subject: originalPostRef, + createdAt, + } as AppBskyFeedLike.Record, + }) + const aliceRepost = await prepareCreate({ + did: sc.dids.alice, + collection: ids.AppBskyFeedRepost, + record: { + $type: ids.AppBskyFeedRepost, + subject: originalPostRef, + createdAt, + } as AppBskyFeedRepost.Record, + }) + + await services.indexing(db).indexRecord(...originalPost) + await services.indexing(db).indexRecord(...ownLike) + await services.indexing(db).indexRecord(...ownRepost) + await services.indexing(db).indexRecord(...aliceLike) + await services.indexing(db).indexRecord(...aliceRepost) + + await network.bsky.processAll() + + const { data: { notifications } } = await agent.api.app.bsky.notification.listNotifications( + {}, + { headers: await network.serviceHeaders(sc.dids.bob) }, + ) + + expect(notifications).toHaveLength(2) + expect(notifications.every(n => { + return n.author.did !== sc.dids.bob + })).toBeTruthy() + + // Cleanup + const del = (uri: AtUri) => { + return prepareDelete({ + did: uri.host, + collection: uri.collection, + rkey: uri.rkey, + }) + } + + await services.indexing(db).deleteRecord(...del(ownLike[0])) + await services.indexing(db).deleteRecord(...del(ownRepost[0])) + await services.indexing(db).deleteRecord(...del(aliceLike[0])) + await services.indexing(db).deleteRecord(...del(aliceRepost[0])) + await services.indexing(db).deleteRecord(...del(originalPost[0])) + }) + it('handles profile aggregations out of order.', async () => { const { db, services } = network.bsky.indexer.ctx const createdAt = new Date().toISOString() diff --git a/packages/pds/src/app-view/services/indexing/plugins/like.ts b/packages/pds/src/app-view/services/indexing/plugins/like.ts index 3adb68cee0c..cd3f7554f15 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/like.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/like.ts @@ -55,7 +55,9 @@ const findDuplicate = async ( const notifsForInsert = (obj: IndexedLike) => { const subjectUri = new AtUri(obj.subject) - return [ + // prevent self-notifications + const isSelf = subjectUri.host === obj.creator + return isSelf ? [] : [ { userDid: subjectUri.host, author: obj.creator, diff --git a/packages/pds/src/app-view/services/indexing/plugins/repost.ts b/packages/pds/src/app-view/services/indexing/plugins/repost.ts index 70acadf84de..d3e1c0c64c0 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/repost.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/repost.ts @@ -74,7 +74,9 @@ const findDuplicate = async ( const notifsForInsert = (obj: IndexedRepost) => { const subjectUri = new AtUri(obj.subject) - return [ + // prevent self-notifications + const isSelf = subjectUri.host === obj.creator + return isSelf ? [] : [ { userDid: subjectUri.host, author: obj.creator, diff --git a/packages/pds/tests/indexing.test.ts b/packages/pds/tests/indexing.test.ts index 7c2c7362079..948a434d9a8 100644 --- a/packages/pds/tests/indexing.test.ts +++ b/packages/pds/tests/indexing.test.ts @@ -1,4 +1,9 @@ -import AtpAgent, { AppBskyActorProfile, AppBskyFeedPost } from '@atproto/api' +import AtpAgent, { + AppBskyActorProfile, + AppBskyFeedPost, + AppBskyFeedLike, + AppBskyFeedRepost, +} from '@atproto/api' import { AtUri } from '@atproto/uri' import { CloseFn, forSnapshot, runTestServer, TestServerInfo } from './_util' import { SeedClient } from './seeds/client' @@ -166,6 +171,136 @@ describe('indexing', () => { ).toMatchSnapshot() }) + it('does not notify user of own like or repost', async () => { + const { db, services } = server.ctx + const createdAt = new Date().toISOString() + + const originalPost = await prepareCreate({ + did: sc.dids.bob, + collection: ids.AppBskyFeedPost, + record: { + $type: ids.AppBskyFeedPost, + text: 'original post', + createdAt, + } as AppBskyFeedPost.Record, + }) + + const originalPostRef = { + uri: originalPost.uri.toString(), + cid: originalPost.cid.toString(), + } + + // own actions + const ownLike = await prepareCreate({ + did: sc.dids.bob, + collection: ids.AppBskyFeedLike, + record: { + $type: ids.AppBskyFeedLike, + subject: originalPostRef, + createdAt, + } as AppBskyFeedLike.Record, + }) + const ownRepost = await prepareCreate({ + did: sc.dids.bob, + collection: ids.AppBskyFeedRepost, + record: { + $type: ids.AppBskyFeedRepost, + subject: originalPostRef, + createdAt, + } as AppBskyFeedRepost.Record, + }) + + // other actions + const aliceLike = await prepareCreate({ + did: sc.dids.alice, + collection: ids.AppBskyFeedLike, + record: { + $type: ids.AppBskyFeedLike, + subject: originalPostRef, + createdAt, + } as AppBskyFeedLike.Record, + }) + const aliceRepost = await prepareCreate({ + did: sc.dids.alice, + collection: ids.AppBskyFeedRepost, + record: { + $type: ids.AppBskyFeedRepost, + subject: originalPostRef, + createdAt, + } as AppBskyFeedRepost.Record, + }) + + await services + .repo(db) + .processWrites( + { + did: sc.dids.bob, + writes: [originalPost, ownLike, ownRepost], + }, + 1, + ) + await services + .repo(db) + .processWrites( + { + did: sc.dids.alice, + writes: [aliceLike, aliceRepost], + }, + 1, + ) + + await server.processAll() + + const { + data: { notifications }, + } = await agent.api.app.bsky.notification.listNotifications( + {}, + { headers: sc.getHeaders(sc.dids.bob) }, + ) + + expect(notifications).toHaveLength(2) + expect(notifications.every(n => { + return n.author.did !== sc.dids.bob + })).toBeTruthy() + + // Cleanup + const del = (uri: AtUri) => { + return prepareDelete({ + did: uri.host, + collection: uri.collection, + rkey: uri.rkey, + }) + } + + // Delete + await services + .repo(db) + .processWrites( + { + did: sc.dids.bob, + writes: [ + del(originalPost.uri), + del(ownLike.uri), + del(ownRepost.uri), + ], + }, + 1, + ) + await services + .repo(db) + .processWrites( + { + did: sc.dids.alice, + writes: [ + del(aliceLike.uri), + del(aliceRepost.uri), + ], + }, + 1, + ) + await server.processAll() + }) + async function getNotifications(db: Database, uri: AtUri) { return await db.db .selectFrom('user_notification') From 896c5b0ad1b1b4ebda053212b695532447345505 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 21 Aug 2023 16:08:29 -0500 Subject: [PATCH 171/237] format --- .../src/services/indexing/plugins/like.ts | 24 +++--- .../src/services/indexing/plugins/repost.ts | 24 +++--- packages/bsky/tests/indexing.test.ts | 12 ++- .../services/indexing/plugins/like.ts | 24 +++--- .../services/indexing/plugins/repost.ts | 24 +++--- packages/pds/tests/indexing.test.ts | 79 ++++++++----------- 6 files changed, 93 insertions(+), 94 deletions(-) diff --git a/packages/bsky/src/services/indexing/plugins/like.ts b/packages/bsky/src/services/indexing/plugins/like.ts index 5bbe9521ada..46466716a07 100644 --- a/packages/bsky/src/services/indexing/plugins/like.ts +++ b/packages/bsky/src/services/indexing/plugins/like.ts @@ -55,17 +55,19 @@ const notifsForInsert = (obj: IndexedLike) => { const subjectUri = new AtUri(obj.subject) // prevent self-notifications const isSelf = subjectUri.host === obj.creator - return isSelf ? [] : [ - { - did: subjectUri.host, - author: obj.creator, - recordUri: obj.uri, - recordCid: obj.cid, - reason: 'like' as const, - reasonSubject: subjectUri.toString(), - sortAt: obj.indexedAt, - }, - ] + return isSelf + ? [] + : [ + { + did: subjectUri.host, + author: obj.creator, + recordUri: obj.uri, + recordCid: obj.cid, + reason: 'like' as const, + reasonSubject: subjectUri.toString(), + sortAt: obj.indexedAt, + }, + ] } const deleteFn = async ( diff --git a/packages/bsky/src/services/indexing/plugins/repost.ts b/packages/bsky/src/services/indexing/plugins/repost.ts index 3813bba698d..63ef69424a3 100644 --- a/packages/bsky/src/services/indexing/plugins/repost.ts +++ b/packages/bsky/src/services/indexing/plugins/repost.ts @@ -74,17 +74,19 @@ const notifsForInsert = (obj: IndexedRepost) => { const subjectUri = new AtUri(obj.subject) // prevent self-notifications const isSelf = subjectUri.host === obj.creator - return isSelf ? [] : [ - { - did: subjectUri.host, - author: obj.creator, - recordUri: obj.uri, - recordCid: obj.cid, - reason: 'repost' as const, - reasonSubject: subjectUri.toString(), - sortAt: obj.indexedAt, - }, - ] + return isSelf + ? [] + : [ + { + did: subjectUri.host, + author: obj.creator, + recordUri: obj.uri, + recordCid: obj.cid, + reason: 'repost' as const, + reasonSubject: subjectUri.toString(), + sortAt: obj.indexedAt, + }, + ] } const deleteFn = async ( diff --git a/packages/bsky/tests/indexing.test.ts b/packages/bsky/tests/indexing.test.ts index 4ca4f729f8b..a4b1d32afca 100644 --- a/packages/bsky/tests/indexing.test.ts +++ b/packages/bsky/tests/indexing.test.ts @@ -334,15 +334,19 @@ describe('indexing', () => { await network.bsky.processAll() - const { data: { notifications } } = await agent.api.app.bsky.notification.listNotifications( + const { + data: { notifications }, + } = await agent.api.app.bsky.notification.listNotifications( {}, { headers: await network.serviceHeaders(sc.dids.bob) }, ) expect(notifications).toHaveLength(2) - expect(notifications.every(n => { - return n.author.did !== sc.dids.bob - })).toBeTruthy() + expect( + notifications.every((n) => { + return n.author.did !== sc.dids.bob + }), + ).toBeTruthy() // Cleanup const del = (uri: AtUri) => { diff --git a/packages/pds/src/app-view/services/indexing/plugins/like.ts b/packages/pds/src/app-view/services/indexing/plugins/like.ts index cd3f7554f15..ce661edc5bb 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/like.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/like.ts @@ -57,17 +57,19 @@ const notifsForInsert = (obj: IndexedLike) => { const subjectUri = new AtUri(obj.subject) // prevent self-notifications const isSelf = subjectUri.host === obj.creator - return isSelf ? [] : [ - { - userDid: subjectUri.host, - author: obj.creator, - recordUri: obj.uri, - recordCid: obj.cid, - reason: 'like' as const, - reasonSubject: subjectUri.toString(), - indexedAt: obj.indexedAt, - }, - ] + return isSelf + ? [] + : [ + { + userDid: subjectUri.host, + author: obj.creator, + recordUri: obj.uri, + recordCid: obj.cid, + reason: 'like' as const, + reasonSubject: subjectUri.toString(), + indexedAt: obj.indexedAt, + }, + ] } const deleteFn = async ( diff --git a/packages/pds/src/app-view/services/indexing/plugins/repost.ts b/packages/pds/src/app-view/services/indexing/plugins/repost.ts index d3e1c0c64c0..bd048d27cff 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/repost.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/repost.ts @@ -76,17 +76,19 @@ const notifsForInsert = (obj: IndexedRepost) => { const subjectUri = new AtUri(obj.subject) // prevent self-notifications const isSelf = subjectUri.host === obj.creator - return isSelf ? [] : [ - { - userDid: subjectUri.host, - author: obj.creator, - recordUri: obj.uri, - recordCid: obj.cid, - reason: 'repost' as const, - reasonSubject: subjectUri.toString(), - indexedAt: obj.indexedAt, - }, - ] + return isSelf + ? [] + : [ + { + userDid: subjectUri.host, + author: obj.creator, + recordUri: obj.uri, + recordCid: obj.cid, + reason: 'repost' as const, + reasonSubject: subjectUri.toString(), + indexedAt: obj.indexedAt, + }, + ] } const deleteFn = async ( diff --git a/packages/pds/tests/indexing.test.ts b/packages/pds/tests/indexing.test.ts index 948a434d9a8..8529bec0ffa 100644 --- a/packages/pds/tests/indexing.test.ts +++ b/packages/pds/tests/indexing.test.ts @@ -230,24 +230,20 @@ describe('indexing', () => { } as AppBskyFeedRepost.Record, }) - await services - .repo(db) - .processWrites( - { - did: sc.dids.bob, - writes: [originalPost, ownLike, ownRepost], - }, - 1, - ) - await services - .repo(db) - .processWrites( - { - did: sc.dids.alice, - writes: [aliceLike, aliceRepost], - }, - 1, - ) + await services.repo(db).processWrites( + { + did: sc.dids.bob, + writes: [originalPost, ownLike, ownRepost], + }, + 1, + ) + await services.repo(db).processWrites( + { + did: sc.dids.alice, + writes: [aliceLike, aliceRepost], + }, + 1, + ) await server.processAll() @@ -259,9 +255,11 @@ describe('indexing', () => { ) expect(notifications).toHaveLength(2) - expect(notifications.every(n => { - return n.author.did !== sc.dids.bob - })).toBeTruthy() + expect( + notifications.every((n) => { + return n.author.did !== sc.dids.bob + }), + ).toBeTruthy() // Cleanup const del = (uri: AtUri) => { @@ -273,31 +271,20 @@ describe('indexing', () => { } // Delete - await services - .repo(db) - .processWrites( - { - did: sc.dids.bob, - writes: [ - del(originalPost.uri), - del(ownLike.uri), - del(ownRepost.uri), - ], - }, - 1, - ) - await services - .repo(db) - .processWrites( - { - did: sc.dids.alice, - writes: [ - del(aliceLike.uri), - del(aliceRepost.uri), - ], - }, - 1, - ) + await services.repo(db).processWrites( + { + did: sc.dids.bob, + writes: [del(originalPost.uri), del(ownLike.uri), del(ownRepost.uri)], + }, + 1, + ) + await services.repo(db).processWrites( + { + did: sc.dids.alice, + writes: [del(aliceLike.uri), del(aliceRepost.uri)], + }, + 1, + ) await server.processAll() }) From 891bf3bb7a8971809fb0caf996c6b30a40f1e963 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 21 Aug 2023 16:10:04 -0500 Subject: [PATCH 172/237] Application ratelimits (#1382) * initial setup * lexgen * tidying things up * add in some rate limiting * testing rate limits * small bugfix * fix build * increase rate limit * more limits * config * optional rate limiter * tweak headers * ratelimit headers * multiple rate limits * tests & bugfixes * test bypass * slight refactor * fail open * fail open * right most xff ip * setup redis for ratelimiting * redis test * more tests * use new dev-infra package * adjust limits * temporarily remove repo write limits * codegen * redis scratch var name * cfg var host -> address --- package.json | 4 +- packages/bsky/src/lexicon/index.ts | 516 ++++++++++++++---- .../types/app/bsky/actor/getPreferences.ts | 7 +- .../types/app/bsky/actor/getProfile.ts | 7 +- .../types/app/bsky/actor/getProfiles.ts | 7 +- .../types/app/bsky/actor/getSuggestions.ts | 7 +- .../types/app/bsky/actor/putPreferences.ts | 7 +- .../types/app/bsky/actor/searchActors.ts | 7 +- .../app/bsky/actor/searchActorsTypeahead.ts | 7 +- .../app/bsky/feed/describeFeedGenerator.ts | 7 +- .../types/app/bsky/feed/getActorFeeds.ts | 7 +- .../types/app/bsky/feed/getAuthorFeed.ts | 7 +- .../lexicon/types/app/bsky/feed/getFeed.ts | 7 +- .../types/app/bsky/feed/getFeedGenerator.ts | 7 +- .../types/app/bsky/feed/getFeedGenerators.ts | 7 +- .../types/app/bsky/feed/getFeedSkeleton.ts | 7 +- .../lexicon/types/app/bsky/feed/getLikes.ts | 7 +- .../types/app/bsky/feed/getPostThread.ts | 7 +- .../lexicon/types/app/bsky/feed/getPosts.ts | 7 +- .../types/app/bsky/feed/getRepostedBy.ts | 7 +- .../types/app/bsky/feed/getTimeline.ts | 7 +- .../lexicon/types/app/bsky/graph/getBlocks.ts | 7 +- .../types/app/bsky/graph/getFollowers.ts | 7 +- .../types/app/bsky/graph/getFollows.ts | 7 +- .../lexicon/types/app/bsky/graph/getList.ts | 7 +- .../types/app/bsky/graph/getListMutes.ts | 7 +- .../lexicon/types/app/bsky/graph/getLists.ts | 7 +- .../lexicon/types/app/bsky/graph/getMutes.ts | 7 +- .../lexicon/types/app/bsky/graph/muteActor.ts | 7 +- .../types/app/bsky/graph/muteActorList.ts | 7 +- .../types/app/bsky/graph/unmuteActor.ts | 7 +- .../types/app/bsky/graph/unmuteActorList.ts | 7 +- .../app/bsky/notification/getUnreadCount.ts | 7 +- .../bsky/notification/listNotifications.ts | 7 +- .../types/app/bsky/notification/updateSeen.ts | 7 +- .../types/app/bsky/unspecced/applyLabels.ts | 7 +- .../types/app/bsky/unspecced/getPopular.ts | 7 +- .../unspecced/getPopularFeedGenerators.ts | 7 +- .../app/bsky/unspecced/getTimelineSkeleton.ts | 7 +- .../atproto/admin/disableAccountInvites.ts | 7 +- .../com/atproto/admin/disableInviteCodes.ts | 7 +- .../com/atproto/admin/enableAccountInvites.ts | 7 +- .../types/com/atproto/admin/getInviteCodes.ts | 7 +- .../com/atproto/admin/getModerationAction.ts | 7 +- .../com/atproto/admin/getModerationActions.ts | 7 +- .../com/atproto/admin/getModerationReport.ts | 7 +- .../com/atproto/admin/getModerationReports.ts | 7 +- .../types/com/atproto/admin/getRecord.ts | 7 +- .../types/com/atproto/admin/getRepo.ts | 7 +- .../types/com/atproto/admin/rebaseRepo.ts | 7 +- .../atproto/admin/resolveModerationReports.ts | 7 +- .../atproto/admin/reverseModerationAction.ts | 7 +- .../types/com/atproto/admin/searchRepos.ts | 7 +- .../types/com/atproto/admin/sendEmail.ts | 7 +- .../com/atproto/admin/takeModerationAction.ts | 7 +- .../com/atproto/admin/updateAccountEmail.ts | 7 +- .../com/atproto/admin/updateAccountHandle.ts | 7 +- .../com/atproto/identity/resolveHandle.ts | 7 +- .../com/atproto/identity/updateHandle.ts | 7 +- .../types/com/atproto/label/queryLabels.ts | 7 +- .../com/atproto/label/subscribeLabels.ts | 7 +- .../com/atproto/moderation/createReport.ts | 7 +- .../types/com/atproto/repo/applyWrites.ts | 7 +- .../types/com/atproto/repo/createRecord.ts | 7 +- .../types/com/atproto/repo/deleteRecord.ts | 7 +- .../types/com/atproto/repo/describeRepo.ts | 7 +- .../types/com/atproto/repo/getRecord.ts | 7 +- .../types/com/atproto/repo/listRecords.ts | 7 +- .../types/com/atproto/repo/putRecord.ts | 7 +- .../types/com/atproto/repo/rebaseRepo.ts | 7 +- .../types/com/atproto/repo/uploadBlob.ts | 7 +- .../types/com/atproto/server/createAccount.ts | 7 +- .../com/atproto/server/createAppPassword.ts | 7 +- .../com/atproto/server/createInviteCode.ts | 7 +- .../com/atproto/server/createInviteCodes.ts | 7 +- .../types/com/atproto/server/createSession.ts | 7 +- .../types/com/atproto/server/deleteAccount.ts | 7 +- .../types/com/atproto/server/deleteSession.ts | 7 +- .../com/atproto/server/describeServer.ts | 7 +- .../atproto/server/getAccountInviteCodes.ts | 7 +- .../types/com/atproto/server/getSession.ts | 7 +- .../com/atproto/server/listAppPasswords.ts | 7 +- .../com/atproto/server/refreshSession.ts | 7 +- .../atproto/server/requestAccountDelete.ts | 7 +- .../atproto/server/requestPasswordReset.ts | 7 +- .../types/com/atproto/server/resetPassword.ts | 7 +- .../com/atproto/server/revokeAppPassword.ts | 7 +- .../lexicon/types/com/atproto/sync/getBlob.ts | 7 +- .../types/com/atproto/sync/getBlocks.ts | 7 +- .../types/com/atproto/sync/getCheckout.ts | 7 +- .../types/com/atproto/sync/getCommitPath.ts | 7 +- .../lexicon/types/com/atproto/sync/getHead.ts | 7 +- .../types/com/atproto/sync/getRecord.ts | 7 +- .../lexicon/types/com/atproto/sync/getRepo.ts | 7 +- .../types/com/atproto/sync/listBlobs.ts | 7 +- .../types/com/atproto/sync/listRepos.ts | 7 +- .../types/com/atproto/sync/notifyOfUpdate.ts | 7 +- .../types/com/atproto/sync/requestCrawl.ts | 7 +- .../types/com/atproto/sync/subscribeRepos.ts | 7 +- packages/dev-env/src/pds.ts | 1 + packages/lex-cli/src/codegen/server.ts | 66 ++- packages/pds/package.json | 1 + .../api/com/atproto/identity/updateHandle.ts | 13 + .../src/api/com/atproto/repo/rebaseRepo.ts | 6 + .../api/com/atproto/server/createAccount.ts | 181 +++--- .../api/com/atproto/server/createSession.ts | 103 ++-- .../api/com/atproto/server/deleteAccount.ts | 131 +++-- .../api/com/atproto/server/resetPassword.ts | 61 ++- packages/pds/src/config.ts | 34 ++ packages/pds/src/context.ts | 6 + packages/pds/src/index.ts | 51 +- packages/pds/src/lexicon/index.ts | 516 ++++++++++++++---- .../types/app/bsky/actor/getPreferences.ts | 7 +- .../types/app/bsky/actor/getProfile.ts | 7 +- .../types/app/bsky/actor/getProfiles.ts | 7 +- .../types/app/bsky/actor/getSuggestions.ts | 7 +- .../types/app/bsky/actor/putPreferences.ts | 7 +- .../types/app/bsky/actor/searchActors.ts | 7 +- .../app/bsky/actor/searchActorsTypeahead.ts | 7 +- .../app/bsky/feed/describeFeedGenerator.ts | 7 +- .../types/app/bsky/feed/getActorFeeds.ts | 7 +- .../types/app/bsky/feed/getAuthorFeed.ts | 7 +- .../lexicon/types/app/bsky/feed/getFeed.ts | 7 +- .../types/app/bsky/feed/getFeedGenerator.ts | 7 +- .../types/app/bsky/feed/getFeedGenerators.ts | 7 +- .../types/app/bsky/feed/getFeedSkeleton.ts | 7 +- .../lexicon/types/app/bsky/feed/getLikes.ts | 7 +- .../types/app/bsky/feed/getPostThread.ts | 7 +- .../lexicon/types/app/bsky/feed/getPosts.ts | 7 +- .../types/app/bsky/feed/getRepostedBy.ts | 7 +- .../types/app/bsky/feed/getTimeline.ts | 7 +- .../lexicon/types/app/bsky/graph/getBlocks.ts | 7 +- .../types/app/bsky/graph/getFollowers.ts | 7 +- .../types/app/bsky/graph/getFollows.ts | 7 +- .../lexicon/types/app/bsky/graph/getList.ts | 7 +- .../types/app/bsky/graph/getListMutes.ts | 7 +- .../lexicon/types/app/bsky/graph/getLists.ts | 7 +- .../lexicon/types/app/bsky/graph/getMutes.ts | 7 +- .../lexicon/types/app/bsky/graph/muteActor.ts | 7 +- .../types/app/bsky/graph/muteActorList.ts | 7 +- .../types/app/bsky/graph/unmuteActor.ts | 7 +- .../types/app/bsky/graph/unmuteActorList.ts | 7 +- .../app/bsky/notification/getUnreadCount.ts | 7 +- .../bsky/notification/listNotifications.ts | 7 +- .../types/app/bsky/notification/updateSeen.ts | 7 +- .../types/app/bsky/unspecced/applyLabels.ts | 7 +- .../types/app/bsky/unspecced/getPopular.ts | 7 +- .../unspecced/getPopularFeedGenerators.ts | 7 +- .../app/bsky/unspecced/getTimelineSkeleton.ts | 7 +- .../atproto/admin/disableAccountInvites.ts | 7 +- .../com/atproto/admin/disableInviteCodes.ts | 7 +- .../com/atproto/admin/enableAccountInvites.ts | 7 +- .../types/com/atproto/admin/getInviteCodes.ts | 7 +- .../com/atproto/admin/getModerationAction.ts | 7 +- .../com/atproto/admin/getModerationActions.ts | 7 +- .../com/atproto/admin/getModerationReport.ts | 7 +- .../com/atproto/admin/getModerationReports.ts | 7 +- .../types/com/atproto/admin/getRecord.ts | 7 +- .../types/com/atproto/admin/getRepo.ts | 7 +- .../types/com/atproto/admin/rebaseRepo.ts | 7 +- .../atproto/admin/resolveModerationReports.ts | 7 +- .../atproto/admin/reverseModerationAction.ts | 7 +- .../types/com/atproto/admin/searchRepos.ts | 7 +- .../types/com/atproto/admin/sendEmail.ts | 7 +- .../com/atproto/admin/takeModerationAction.ts | 7 +- .../com/atproto/admin/updateAccountEmail.ts | 7 +- .../com/atproto/admin/updateAccountHandle.ts | 7 +- .../com/atproto/identity/resolveHandle.ts | 7 +- .../com/atproto/identity/updateHandle.ts | 7 +- .../types/com/atproto/label/queryLabels.ts | 7 +- .../com/atproto/label/subscribeLabels.ts | 7 +- .../com/atproto/moderation/createReport.ts | 7 +- .../types/com/atproto/repo/applyWrites.ts | 7 +- .../types/com/atproto/repo/createRecord.ts | 7 +- .../types/com/atproto/repo/deleteRecord.ts | 7 +- .../types/com/atproto/repo/describeRepo.ts | 7 +- .../types/com/atproto/repo/getRecord.ts | 7 +- .../types/com/atproto/repo/listRecords.ts | 7 +- .../types/com/atproto/repo/putRecord.ts | 7 +- .../types/com/atproto/repo/rebaseRepo.ts | 7 +- .../types/com/atproto/repo/uploadBlob.ts | 7 +- .../types/com/atproto/server/createAccount.ts | 7 +- .../com/atproto/server/createAppPassword.ts | 7 +- .../com/atproto/server/createInviteCode.ts | 7 +- .../com/atproto/server/createInviteCodes.ts | 7 +- .../types/com/atproto/server/createSession.ts | 7 +- .../types/com/atproto/server/deleteAccount.ts | 7 +- .../types/com/atproto/server/deleteSession.ts | 7 +- .../com/atproto/server/describeServer.ts | 7 +- .../atproto/server/getAccountInviteCodes.ts | 7 +- .../types/com/atproto/server/getSession.ts | 7 +- .../com/atproto/server/listAppPasswords.ts | 7 +- .../com/atproto/server/refreshSession.ts | 7 +- .../atproto/server/requestAccountDelete.ts | 7 +- .../atproto/server/requestPasswordReset.ts | 7 +- .../types/com/atproto/server/resetPassword.ts | 7 +- .../com/atproto/server/revokeAppPassword.ts | 7 +- .../lexicon/types/com/atproto/sync/getBlob.ts | 7 +- .../types/com/atproto/sync/getBlocks.ts | 7 +- .../types/com/atproto/sync/getCheckout.ts | 7 +- .../types/com/atproto/sync/getCommitPath.ts | 7 +- .../lexicon/types/com/atproto/sync/getHead.ts | 7 +- .../types/com/atproto/sync/getRecord.ts | 7 +- .../lexicon/types/com/atproto/sync/getRepo.ts | 7 +- .../types/com/atproto/sync/listBlobs.ts | 7 +- .../types/com/atproto/sync/listRepos.ts | 7 +- .../types/com/atproto/sync/notifyOfUpdate.ts | 7 +- .../types/com/atproto/sync/requestCrawl.ts | 7 +- .../types/com/atproto/sync/subscribeRepos.ts | 7 +- packages/pds/src/logger.ts | 1 + packages/pds/src/redis.ts | 25 + packages/pds/tests/_util.ts | 1 + packages/pds/tests/rate-limits.test.ts | 69 +++ packages/xrpc-server/package.json | 1 + packages/xrpc-server/src/index.ts | 1 + packages/xrpc-server/src/rate-limiter.ts | 159 ++++++ packages/xrpc-server/src/server.ts | 96 +++- packages/xrpc-server/src/types.ts | 82 ++- packages/xrpc-server/src/util.ts | 4 + .../xrpc-server/tests/rate-limiter.test.ts | 249 +++++++++ yarn.lock | 5 + 221 files changed, 2926 insertions(+), 815 deletions(-) create mode 100644 packages/pds/src/redis.ts create mode 100644 packages/pds/tests/rate-limits.test.ts create mode 100644 packages/xrpc-server/src/rate-limiter.ts create mode 100644 packages/xrpc-server/tests/rate-limiter.test.ts diff --git a/package.json b/package.json index 45fbd5ad7ae..93475e7a9c0 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "verify": "lerna run verify --stream", "prettier": "lerna run prettier", "build": "lerna run build", - "test": "LOG_ENABLED=false NODE_ENV=development ./packages/dev-infra/with-test-db.sh lerna run test --stream", - "test:withFlags": "LOG_ENABLED=false NODE_ENV=development ./packages/dev-infra/with-test-db.sh lerna run test --stream --" + "test": "LOG_ENABLED=false NODE_ENV=development ./packages/dev-infra/with-test-redis-and-db.sh lerna run test --stream", + "test:withFlags": "LOG_ENABLED=false NODE_ENV=development ./packages/dev-infra/with-test-redis-and-db.sh lerna run test --stream --" }, "devDependencies": { "@babel/core": "^7.18.6", diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 17c90d9ec8d..cd0594afef1 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -183,7 +183,8 @@ export class AdminNS { disableAccountInvites( cfg: ConfigOf< AV, - ComAtprotoAdminDisableAccountInvites.Handler> + ComAtprotoAdminDisableAccountInvites.Handler>, + ComAtprotoAdminDisableAccountInvites.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.disableAccountInvites' // @ts-ignore @@ -193,7 +194,8 @@ export class AdminNS { disableInviteCodes( cfg: ConfigOf< AV, - ComAtprotoAdminDisableInviteCodes.Handler> + ComAtprotoAdminDisableInviteCodes.Handler>, + ComAtprotoAdminDisableInviteCodes.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.disableInviteCodes' // @ts-ignore @@ -203,7 +205,8 @@ export class AdminNS { enableAccountInvites( cfg: ConfigOf< AV, - ComAtprotoAdminEnableAccountInvites.Handler> + ComAtprotoAdminEnableAccountInvites.Handler>, + ComAtprotoAdminEnableAccountInvites.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.enableAccountInvites' // @ts-ignore @@ -211,7 +214,11 @@ export class AdminNS { } getInviteCodes( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoAdminGetInviteCodes.Handler>, + ComAtprotoAdminGetInviteCodes.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.admin.getInviteCodes' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -220,7 +227,8 @@ export class AdminNS { getModerationAction( cfg: ConfigOf< AV, - ComAtprotoAdminGetModerationAction.Handler> + ComAtprotoAdminGetModerationAction.Handler>, + ComAtprotoAdminGetModerationAction.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.getModerationAction' // @ts-ignore @@ -230,7 +238,8 @@ export class AdminNS { getModerationActions( cfg: ConfigOf< AV, - ComAtprotoAdminGetModerationActions.Handler> + ComAtprotoAdminGetModerationActions.Handler>, + ComAtprotoAdminGetModerationActions.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.getModerationActions' // @ts-ignore @@ -240,7 +249,8 @@ export class AdminNS { getModerationReport( cfg: ConfigOf< AV, - ComAtprotoAdminGetModerationReport.Handler> + ComAtprotoAdminGetModerationReport.Handler>, + ComAtprotoAdminGetModerationReport.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.getModerationReport' // @ts-ignore @@ -250,7 +260,8 @@ export class AdminNS { getModerationReports( cfg: ConfigOf< AV, - ComAtprotoAdminGetModerationReports.Handler> + ComAtprotoAdminGetModerationReports.Handler>, + ComAtprotoAdminGetModerationReports.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.getModerationReports' // @ts-ignore @@ -258,21 +269,33 @@ export class AdminNS { } getRecord( - cfg: ConfigOf>>, + 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>>, + cfg: ConfigOf< + AV, + ComAtprotoAdminGetRepo.Handler>, + ComAtprotoAdminGetRepo.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.admin.getRepo' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } rebaseRepo( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoAdminRebaseRepo.Handler>, + ComAtprotoAdminRebaseRepo.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.admin.rebaseRepo' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -281,7 +304,8 @@ export class AdminNS { resolveModerationReports( cfg: ConfigOf< AV, - ComAtprotoAdminResolveModerationReports.Handler> + ComAtprotoAdminResolveModerationReports.Handler>, + ComAtprotoAdminResolveModerationReports.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.resolveModerationReports' // @ts-ignore @@ -291,7 +315,8 @@ export class AdminNS { reverseModerationAction( cfg: ConfigOf< AV, - ComAtprotoAdminReverseModerationAction.Handler> + ComAtprotoAdminReverseModerationAction.Handler>, + ComAtprotoAdminReverseModerationAction.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.reverseModerationAction' // @ts-ignore @@ -299,14 +324,22 @@ export class AdminNS { } searchRepos( - cfg: ConfigOf>>, + 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>>, + cfg: ConfigOf< + AV, + ComAtprotoAdminSendEmail.Handler>, + ComAtprotoAdminSendEmail.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.admin.sendEmail' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -315,7 +348,8 @@ export class AdminNS { takeModerationAction( cfg: ConfigOf< AV, - ComAtprotoAdminTakeModerationAction.Handler> + ComAtprotoAdminTakeModerationAction.Handler>, + ComAtprotoAdminTakeModerationAction.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.takeModerationAction' // @ts-ignore @@ -325,7 +359,8 @@ export class AdminNS { updateAccountEmail( cfg: ConfigOf< AV, - ComAtprotoAdminUpdateAccountEmail.Handler> + ComAtprotoAdminUpdateAccountEmail.Handler>, + ComAtprotoAdminUpdateAccountEmail.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.updateAccountEmail' // @ts-ignore @@ -335,7 +370,8 @@ export class AdminNS { updateAccountHandle( cfg: ConfigOf< AV, - ComAtprotoAdminUpdateAccountHandle.Handler> + ComAtprotoAdminUpdateAccountHandle.Handler>, + ComAtprotoAdminUpdateAccountHandle.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.updateAccountHandle' // @ts-ignore @@ -351,14 +387,22 @@ export class IdentityNS { } resolveHandle( - cfg: ConfigOf>>, + 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>>, + cfg: ConfigOf< + AV, + ComAtprotoIdentityUpdateHandle.Handler>, + ComAtprotoIdentityUpdateHandle.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.identity.updateHandle' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -373,14 +417,22 @@ export class LabelNS { } queryLabels( - cfg: ConfigOf>>, + 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>>, + cfg: ConfigOf< + AV, + ComAtprotoLabelSubscribeLabels.Handler>, + ComAtprotoLabelSubscribeLabels.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.label.subscribeLabels' // @ts-ignore return this._server.xrpc.streamMethod(nsid, cfg) @@ -397,7 +449,8 @@ export class ModerationNS { createReport( cfg: ConfigOf< AV, - ComAtprotoModerationCreateReport.Handler> + ComAtprotoModerationCreateReport.Handler>, + ComAtprotoModerationCreateReport.HandlerReqCtx> >, ) { const nsid = 'com.atproto.moderation.createReport' // @ts-ignore @@ -413,63 +466,99 @@ export class RepoNS { } applyWrites( - cfg: ConfigOf>>, + 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>>, + 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>>, + 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>>, + 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>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + ComAtprotoRepoPutRecord.Handler>, + ComAtprotoRepoPutRecord.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.repo.putRecord' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } rebaseRepo( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoRepoRebaseRepo.Handler>, + ComAtprotoRepoRebaseRepo.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.repo.rebaseRepo' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } uploadBlob( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoRepoUploadBlob.Handler>, + ComAtprotoRepoUploadBlob.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.repo.uploadBlob' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -484,7 +573,11 @@ export class ServerNS { } createAccount( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoServerCreateAccount.Handler>, + ComAtprotoServerCreateAccount.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.server.createAccount' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -493,7 +586,8 @@ export class ServerNS { createAppPassword( cfg: ConfigOf< AV, - ComAtprotoServerCreateAppPassword.Handler> + ComAtprotoServerCreateAppPassword.Handler>, + ComAtprotoServerCreateAppPassword.HandlerReqCtx> >, ) { const nsid = 'com.atproto.server.createAppPassword' // @ts-ignore @@ -503,7 +597,8 @@ export class ServerNS { createInviteCode( cfg: ConfigOf< AV, - ComAtprotoServerCreateInviteCode.Handler> + ComAtprotoServerCreateInviteCode.Handler>, + ComAtprotoServerCreateInviteCode.HandlerReqCtx> >, ) { const nsid = 'com.atproto.server.createInviteCode' // @ts-ignore @@ -513,7 +608,8 @@ export class ServerNS { createInviteCodes( cfg: ConfigOf< AV, - ComAtprotoServerCreateInviteCodes.Handler> + ComAtprotoServerCreateInviteCodes.Handler>, + ComAtprotoServerCreateInviteCodes.HandlerReqCtx> >, ) { const nsid = 'com.atproto.server.createInviteCodes' // @ts-ignore @@ -521,28 +617,44 @@ export class ServerNS { } createSession( - cfg: ConfigOf>>, + 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>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + ComAtprotoServerDescribeServer.Handler>, + ComAtprotoServerDescribeServer.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.server.describeServer' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -551,7 +663,8 @@ export class ServerNS { getAccountInviteCodes( cfg: ConfigOf< AV, - ComAtprotoServerGetAccountInviteCodes.Handler> + ComAtprotoServerGetAccountInviteCodes.Handler>, + ComAtprotoServerGetAccountInviteCodes.HandlerReqCtx> >, ) { const nsid = 'com.atproto.server.getAccountInviteCodes' // @ts-ignore @@ -559,7 +672,11 @@ export class ServerNS { } getSession( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoServerGetSession.Handler>, + ComAtprotoServerGetSession.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.server.getSession' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -568,7 +685,8 @@ export class ServerNS { listAppPasswords( cfg: ConfigOf< AV, - ComAtprotoServerListAppPasswords.Handler> + ComAtprotoServerListAppPasswords.Handler>, + ComAtprotoServerListAppPasswords.HandlerReqCtx> >, ) { const nsid = 'com.atproto.server.listAppPasswords' // @ts-ignore @@ -576,7 +694,11 @@ export class ServerNS { } refreshSession( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoServerRefreshSession.Handler>, + ComAtprotoServerRefreshSession.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.server.refreshSession' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -585,7 +707,8 @@ export class ServerNS { requestAccountDelete( cfg: ConfigOf< AV, - ComAtprotoServerRequestAccountDelete.Handler> + ComAtprotoServerRequestAccountDelete.Handler>, + ComAtprotoServerRequestAccountDelete.HandlerReqCtx> >, ) { const nsid = 'com.atproto.server.requestAccountDelete' // @ts-ignore @@ -595,7 +718,8 @@ export class ServerNS { requestPasswordReset( cfg: ConfigOf< AV, - ComAtprotoServerRequestPasswordReset.Handler> + ComAtprotoServerRequestPasswordReset.Handler>, + ComAtprotoServerRequestPasswordReset.HandlerReqCtx> >, ) { const nsid = 'com.atproto.server.requestPasswordReset' // @ts-ignore @@ -603,7 +727,11 @@ export class ServerNS { } resetPassword( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoServerResetPassword.Handler>, + ComAtprotoServerResetPassword.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.server.resetPassword' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -612,7 +740,8 @@ export class ServerNS { revokeAppPassword( cfg: ConfigOf< AV, - ComAtprotoServerRevokeAppPassword.Handler> + ComAtprotoServerRevokeAppPassword.Handler>, + ComAtprotoServerRevokeAppPassword.HandlerReqCtx> >, ) { const nsid = 'com.atproto.server.revokeAppPassword' // @ts-ignore @@ -628,84 +757,132 @@ export class SyncNS { } getBlob( - cfg: ConfigOf>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + ComAtprotoSyncGetCheckout.Handler>, + ComAtprotoSyncGetCheckout.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.sync.getCheckout' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } getCommitPath( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoSyncGetCommitPath.Handler>, + ComAtprotoSyncGetCommitPath.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.sync.getCommitPath' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } getHead( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoSyncGetHead.Handler>, + ComAtprotoSyncGetHead.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.sync.getHead' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } getRecord( - cfg: ConfigOf>>, + 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>>, + 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>>, + 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>>, + 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>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + ComAtprotoSyncSubscribeRepos.Handler>, + ComAtprotoSyncSubscribeRepos.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.sync.subscribeRepos' // @ts-ignore return this._server.xrpc.streamMethod(nsid, cfg) @@ -752,42 +929,66 @@ export class ActorNS { } getPreferences( - cfg: ConfigOf>>, + 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>>, + 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>>, + 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>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + AppBskyActorSearchActors.Handler>, + AppBskyActorSearchActors.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.actor.searchActors' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -796,7 +997,8 @@ export class ActorNS { searchActorsTypeahead( cfg: ConfigOf< AV, - AppBskyActorSearchActorsTypeahead.Handler> + AppBskyActorSearchActorsTypeahead.Handler>, + AppBskyActorSearchActorsTypeahead.HandlerReqCtx> >, ) { const nsid = 'app.bsky.actor.searchActorsTypeahead' // @ts-ignore @@ -822,7 +1024,8 @@ export class FeedNS { describeFeedGenerator( cfg: ConfigOf< AV, - AppBskyFeedDescribeFeedGenerator.Handler> + AppBskyFeedDescribeFeedGenerator.Handler>, + AppBskyFeedDescribeFeedGenerator.HandlerReqCtx> >, ) { const nsid = 'app.bsky.feed.describeFeedGenerator' // @ts-ignore @@ -830,77 +1033,121 @@ export class FeedNS { } getActorFeeds( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + AppBskyFeedGetActorFeeds.Handler>, + AppBskyFeedGetActorFeeds.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.feed.getActorFeeds' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } getAuthorFeed( - cfg: ConfigOf>>, + 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>>, + 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>>, + 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>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + AppBskyFeedGetLikes.Handler>, + AppBskyFeedGetLikes.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.feed.getLikes' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } getPostThread( - cfg: ConfigOf>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + AppBskyFeedGetRepostedBy.Handler>, + AppBskyFeedGetRepostedBy.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.feed.getRepostedBy' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } getTimeline( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + AppBskyFeedGetTimeline.Handler>, + AppBskyFeedGetTimeline.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.feed.getTimeline' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -915,77 +1162,121 @@ export class GraphNS { } getBlocks( - cfg: ConfigOf>>, + 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>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + AppBskyGraphGetList.Handler>, + AppBskyGraphGetList.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.graph.getList' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } getListMutes( - cfg: ConfigOf>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + AppBskyGraphGetMutes.Handler>, + AppBskyGraphGetMutes.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.graph.getMutes' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } muteActor( - cfg: ConfigOf>>, + 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>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + AppBskyGraphUnmuteActorList.Handler>, + AppBskyGraphUnmuteActorList.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.graph.unmuteActorList' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -1002,7 +1293,8 @@ export class NotificationNS { getUnreadCount( cfg: ConfigOf< AV, - AppBskyNotificationGetUnreadCount.Handler> + AppBskyNotificationGetUnreadCount.Handler>, + AppBskyNotificationGetUnreadCount.HandlerReqCtx> >, ) { const nsid = 'app.bsky.notification.getUnreadCount' // @ts-ignore @@ -1012,7 +1304,8 @@ export class NotificationNS { listNotifications( cfg: ConfigOf< AV, - AppBskyNotificationListNotifications.Handler> + AppBskyNotificationListNotifications.Handler>, + AppBskyNotificationListNotifications.HandlerReqCtx> >, ) { const nsid = 'app.bsky.notification.listNotifications' // @ts-ignore @@ -1020,7 +1313,11 @@ export class NotificationNS { } updateSeen( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + AppBskyNotificationUpdateSeen.Handler>, + AppBskyNotificationUpdateSeen.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.notification.updateSeen' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -1043,14 +1340,22 @@ export class UnspeccedNS { } applyLabels( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + AppBskyUnspeccedApplyLabels.Handler>, + AppBskyUnspeccedApplyLabels.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.unspecced.applyLabels' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } getPopular( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + AppBskyUnspeccedGetPopular.Handler>, + AppBskyUnspeccedGetPopular.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.unspecced.getPopular' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -1059,7 +1364,8 @@ export class UnspeccedNS { getPopularFeedGenerators( cfg: ConfigOf< AV, - AppBskyUnspeccedGetPopularFeedGenerators.Handler> + AppBskyUnspeccedGetPopularFeedGenerators.Handler>, + AppBskyUnspeccedGetPopularFeedGenerators.HandlerReqCtx> >, ) { const nsid = 'app.bsky.unspecced.getPopularFeedGenerators' // @ts-ignore @@ -1069,7 +1375,8 @@ export class UnspeccedNS { getTimelineSkeleton( cfg: ConfigOf< AV, - AppBskyUnspeccedGetTimelineSkeleton.Handler> + AppBskyUnspeccedGetTimelineSkeleton.Handler>, + AppBskyUnspeccedGetTimelineSkeleton.HandlerReqCtx> >, ) { const nsid = 'app.bsky.unspecced.getTimelineSkeleton' // @ts-ignore @@ -1077,10 +1384,23 @@ export class UnspeccedNS { } } -type ConfigOf = +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 HandlerRateLimitOpts = SharedRateLimitOpts | RouteRateLimitOpts +type ConfigOf = | Handler | { auth?: Auth + rateLimit?: HandlerRateLimitOpts | HandlerRateLimitOpts[] handler: Handler } type ExtractAuth = Extract< diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/getPreferences.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/getPreferences.ts index 9ba813a4313..88d78a57cba 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/getPreferences.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/getPreferences.ts @@ -32,10 +32,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/getProfile.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/getProfile.ts index 4a2ef252e7e..802afda5361 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/getProfile.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/getProfile.ts @@ -29,10 +29,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/getProfiles.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/getProfiles.ts index 89c014ebe0a..2549b264e33 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/getProfiles.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/getProfiles.ts @@ -34,10 +34,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/getSuggestions.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/getSuggestions.ts index 5e59cf571d5..a6d4d6102af 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/getSuggestions.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/getSuggestions.ts @@ -36,10 +36,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/putPreferences.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/putPreferences.ts index ba0531cc3c0..1e5ee2d834e 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/putPreferences.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/putPreferences.ts @@ -27,10 +27,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/searchActors.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/searchActors.ts index 0b425ba4df7..f620a463cff 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/searchActors.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/searchActors.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts index 43a6be287b4..4f5bbb7c23c 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts @@ -35,10 +35,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts index 9444257c905..d329bf20a5a 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts @@ -33,13 +33,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface Feed { uri: string diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getActorFeeds.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getActorFeeds.ts index 2a1e9edb889..3e930cbe201 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getActorFeeds.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getActorFeeds.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts index 8c44be045ac..cd66ef5c392 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts @@ -43,10 +43,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getFeed.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getFeed.ts index 837a6d9a892..e72b1010aea 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getFeed.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getFeed.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts index 859f0c70b84..fab3b30c316 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts @@ -36,10 +36,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedGenerators.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedGenerators.ts index 85cbb544721..d7e082f2362 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedGenerators.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedGenerators.ts @@ -34,10 +34,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts index fe132e926f6..1c8f349b42b 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getLikes.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getLikes.ts index deb95fb063a..d581f5bfa9c 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getLikes.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getLikes.ts @@ -40,13 +40,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface Like { indexedAt: string diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getPostThread.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getPostThread.ts index e9f154a86a6..61de94b729d 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getPostThread.ts @@ -41,10 +41,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getPosts.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getPosts.ts index 3cf5747ec1f..4282f5d349f 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getPosts.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getPosts.ts @@ -34,10 +34,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getRepostedBy.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getRepostedBy.ts index 7f7d51a77af..0b9c1a6f68b 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getRepostedBy.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getRepostedBy.ts @@ -40,10 +40,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getTimeline.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getTimeline.ts index 6be4293374c..832caf5c6f7 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getTimeline.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getTimeline.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/getBlocks.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/getBlocks.ts index f3a6eb4b55c..d380a14880a 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getBlocks.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getBlocks.ts @@ -36,10 +36,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/getFollowers.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/getFollowers.ts index e6bec023938..b337be52c1b 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getFollowers.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getFollowers.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/getFollows.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/getFollows.ts index 96c18201e1b..71e9ca0270c 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getFollows.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getFollows.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/getList.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/getList.ts index d0cfe398adf..fc45dd20985 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getList.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getList.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/getListMutes.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/getListMutes.ts index b0820a7ea53..04cca70b44d 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getListMutes.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getListMutes.ts @@ -36,10 +36,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/getLists.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/getLists.ts index b6626cbe096..8acf9362c00 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getLists.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getLists.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/getMutes.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/getMutes.ts index 32956284d7d..0034095b975 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/getMutes.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/getMutes.ts @@ -36,10 +36,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/muteActor.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/muteActor.ts index fd19f661533..52d1b864989 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/muteActor.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/muteActor.ts @@ -26,10 +26,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/muteActorList.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/muteActorList.ts index e099011b3f7..bf803f388af 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/muteActorList.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/muteActorList.ts @@ -26,10 +26,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/unmuteActor.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/unmuteActor.ts index fd19f661533..52d1b864989 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/unmuteActor.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/unmuteActor.ts @@ -26,10 +26,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/unmuteActorList.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/unmuteActorList.ts index e099011b3f7..bf803f388af 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/unmuteActorList.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/unmuteActorList.ts @@ -26,10 +26,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/notification/getUnreadCount.ts b/packages/bsky/src/lexicon/types/app/bsky/notification/getUnreadCount.ts index 365becb061b..6cf3c84beb5 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/notification/getUnreadCount.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/notification/getUnreadCount.ts @@ -33,10 +33,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/notification/listNotifications.ts b/packages/bsky/src/lexicon/types/app/bsky/notification/listNotifications.ts index 02df7ade82e..156ba349ec4 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/notification/listNotifications.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/notification/listNotifications.ts @@ -38,13 +38,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface Notification { uri: string diff --git a/packages/bsky/src/lexicon/types/app/bsky/notification/updateSeen.ts b/packages/bsky/src/lexicon/types/app/bsky/notification/updateSeen.ts index 177f1f4bdbc..136191edc40 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/notification/updateSeen.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/notification/updateSeen.ts @@ -26,10 +26,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/applyLabels.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/applyLabels.ts index 1c07d41d040..1d359a9547d 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/unspecced/applyLabels.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/applyLabels.ts @@ -27,10 +27,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopular.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopular.ts index 44cfe695920..8471ed77a6c 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopular.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopular.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts index 58f385b958e..97937e926c2 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts index 1178a844a93..4ccad20c902 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts index 47b3e6a2f55..051fabb65e1 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts @@ -28,10 +28,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/disableInviteCodes.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/disableInviteCodes.ts index 2e9d326afe9..2b64371f1ed 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/disableInviteCodes.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/disableInviteCodes.ts @@ -27,10 +27,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts index 7f437adfb44..4a26d302333 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts @@ -28,10 +28,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/getInviteCodes.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/getInviteCodes.ts index e3f3fb6e739..1eb099aae66 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/getInviteCodes.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getInviteCodes.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationAction.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationAction.ts index f774416a083..2ab52f237cc 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationAction.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationAction.ts @@ -29,10 +29,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationActions.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationActions.ts index b8a1e683ec5..4c29f965df6 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationActions.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationActions.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationReport.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationReport.ts index 63a1a551f97..28d714453f2 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationReport.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationReport.ts @@ -29,10 +29,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationReports.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationReports.ts index f3372b96cc8..d50af44c757 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationReports.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getModerationReports.ts @@ -51,10 +51,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/getRecord.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/getRecord.ts index 77895570d96..48222d9d819 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/getRecord.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getRecord.ts @@ -31,10 +31,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/getRepo.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/getRepo.ts index 90aca8bdce5..19911baa90a 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/getRepo.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/getRepo.ts @@ -30,10 +30,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/rebaseRepo.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/rebaseRepo.ts index d94a817f47c..d2fe04b1386 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/rebaseRepo.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/rebaseRepo.ts @@ -30,10 +30,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/resolveModerationReports.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/resolveModerationReports.ts index b8558d4e2b7..e3f4d028202 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/resolveModerationReports.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/resolveModerationReports.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/reverseModerationAction.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/reverseModerationAction.ts index 8f17d275761..17dcb5085de 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/reverseModerationAction.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/reverseModerationAction.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/searchRepos.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/searchRepos.ts index 4dc0570276b..c79cd046ca0 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/searchRepos.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/searchRepos.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/sendEmail.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/sendEmail.ts index 356d4513d1a..87e7ceec172 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/sendEmail.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/sendEmail.ts @@ -39,10 +39,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/takeModerationAction.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/takeModerationAction.ts index c2cab3f1928..fbbf14dff0f 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/takeModerationAction.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/takeModerationAction.ts @@ -52,10 +52,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts index eb00af00727..9e6140256ef 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts @@ -28,10 +28,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountHandle.ts b/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountHandle.ts index b9bb76a9c06..c378f421926 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountHandle.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/admin/updateAccountHandle.ts @@ -27,10 +27,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/identity/resolveHandle.ts b/packages/bsky/src/lexicon/types/com/atproto/identity/resolveHandle.ts index bdcd0ac45d9..ef90e99bb30 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/identity/resolveHandle.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/identity/resolveHandle.ts @@ -34,10 +34,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/identity/updateHandle.ts b/packages/bsky/src/lexicon/types/com/atproto/identity/updateHandle.ts index c0c3cf523c5..1f639c344e9 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/identity/updateHandle.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/identity/updateHandle.ts @@ -26,10 +26,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/label/queryLabels.ts b/packages/bsky/src/lexicon/types/com/atproto/label/queryLabels.ts index 791e474c50b..72cf5c52be6 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/label/queryLabels.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/label/queryLabels.ts @@ -40,10 +40,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/label/subscribeLabels.ts b/packages/bsky/src/lexicon/types/com/atproto/label/subscribeLabels.ts index 758407e0437..9d4b4441ae0 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/label/subscribeLabels.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/label/subscribeLabels.ts @@ -20,12 +20,15 @@ export type OutputSchema = | { $type: string; [k: string]: unknown } export type HandlerError = ErrorFrame<'FutureCursor'> export type HandlerOutput = HandlerError | OutputSchema -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams req: IncomingMessage signal: AbortSignal -}) => AsyncIterable +} +export type Handler = ( + ctx: HandlerReqCtx, +) => AsyncIterable export interface Labels { seq: number diff --git a/packages/bsky/src/lexicon/types/com/atproto/moderation/createReport.ts b/packages/bsky/src/lexicon/types/com/atproto/moderation/createReport.ts index a1be0464bf3..96aaf4a9c29 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/moderation/createReport.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/moderation/createReport.ts @@ -53,10 +53,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/repo/applyWrites.ts b/packages/bsky/src/lexicon/types/com/atproto/repo/applyWrites.ts index 6fba024d715..53f2972e116 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/applyWrites.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/applyWrites.ts @@ -32,13 +32,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput /** Create a new record. */ export interface Create { diff --git a/packages/bsky/src/lexicon/types/com/atproto/repo/createRecord.ts b/packages/bsky/src/lexicon/types/com/atproto/repo/createRecord.ts index f731dee536a..e069f8caf74 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/createRecord.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/createRecord.ts @@ -50,10 +50,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/repo/deleteRecord.ts b/packages/bsky/src/lexicon/types/com/atproto/repo/deleteRecord.ts index 016547e4075..5ee016cbed1 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/deleteRecord.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/deleteRecord.ts @@ -36,10 +36,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/repo/describeRepo.ts b/packages/bsky/src/lexicon/types/com/atproto/repo/describeRepo.ts index 6c1300daba4..7b8a2b995eb 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/describeRepo.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/describeRepo.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/repo/getRecord.ts b/packages/bsky/src/lexicon/types/com/atproto/repo/getRecord.ts index ecabf517539..35c9b4b7166 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/getRecord.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/getRecord.ts @@ -42,10 +42,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/repo/listRecords.ts b/packages/bsky/src/lexicon/types/com/atproto/repo/listRecords.ts index 76c3429833c..e58d9714e33 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/listRecords.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/listRecords.ts @@ -46,13 +46,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface Record { uri: string diff --git a/packages/bsky/src/lexicon/types/com/atproto/repo/putRecord.ts b/packages/bsky/src/lexicon/types/com/atproto/repo/putRecord.ts index a56b6ca25f1..364eb59f6f1 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/putRecord.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/putRecord.ts @@ -52,10 +52,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/repo/rebaseRepo.ts b/packages/bsky/src/lexicon/types/com/atproto/repo/rebaseRepo.ts index d94a817f47c..d2fe04b1386 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/rebaseRepo.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/rebaseRepo.ts @@ -30,10 +30,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/repo/uploadBlob.ts b/packages/bsky/src/lexicon/types/com/atproto/repo/uploadBlob.ts index b3f06f30f39..ad6002df925 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/uploadBlob.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/repo/uploadBlob.ts @@ -35,10 +35,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/createAccount.ts b/packages/bsky/src/lexicon/types/com/atproto/server/createAccount.ts index 646091fde1e..c67e7445bf9 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/createAccount.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/createAccount.ts @@ -53,10 +53,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/createAppPassword.ts b/packages/bsky/src/lexicon/types/com/atproto/server/createAppPassword.ts index 39eb73140c6..8e4a0a519e0 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/createAppPassword.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/createAppPassword.ts @@ -35,13 +35,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface AppPassword { name: string diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/createInviteCode.ts b/packages/bsky/src/lexicon/types/com/atproto/server/createInviteCode.ts index 10fef7ca0e4..acfac56ba76 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/createInviteCode.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/createInviteCode.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/createInviteCodes.ts b/packages/bsky/src/lexicon/types/com/atproto/server/createInviteCodes.ts index 98d6585ea06..5887d77fada 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/createInviteCodes.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/createInviteCodes.ts @@ -39,13 +39,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface AccountCodes { account: string diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts b/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts index 1b184da071f..b836551f301 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/createSession.ts @@ -44,10 +44,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/deleteAccount.ts b/packages/bsky/src/lexicon/types/com/atproto/server/deleteAccount.ts index 6fde5f17e90..37ddbba13e0 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/deleteAccount.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/deleteAccount.ts @@ -29,10 +29,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/deleteSession.ts b/packages/bsky/src/lexicon/types/com/atproto/server/deleteSession.ts index 909c2be8cab..e4244870425 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/deleteSession.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/deleteSession.ts @@ -19,10 +19,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/describeServer.ts b/packages/bsky/src/lexicon/types/com/atproto/server/describeServer.ts index 9d65f932ec2..bc73d541a04 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/describeServer.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/describeServer.ts @@ -33,13 +33,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface Links { privacyPolicy?: string diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/getAccountInviteCodes.ts b/packages/bsky/src/lexicon/types/com/atproto/server/getAccountInviteCodes.ts index d0a9dc34307..e387a5e38e4 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/getAccountInviteCodes.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/getAccountInviteCodes.ts @@ -36,10 +36,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/getSession.ts b/packages/bsky/src/lexicon/types/com/atproto/server/getSession.ts index c0f868ca0c1..388fb5eae9d 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/getSession.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/getSession.ts @@ -33,10 +33,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/listAppPasswords.ts b/packages/bsky/src/lexicon/types/com/atproto/server/listAppPasswords.ts index ca0d4e80839..ebd74da9d39 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/listAppPasswords.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/listAppPasswords.ts @@ -32,13 +32,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface AppPassword { name: string diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/refreshSession.ts b/packages/bsky/src/lexicon/types/com/atproto/server/refreshSession.ts index 56e34eaa71f..e47bf09fbc2 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/refreshSession.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/refreshSession.ts @@ -35,10 +35,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/requestAccountDelete.ts b/packages/bsky/src/lexicon/types/com/atproto/server/requestAccountDelete.ts index 909c2be8cab..e4244870425 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/requestAccountDelete.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/requestAccountDelete.ts @@ -19,10 +19,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/requestPasswordReset.ts b/packages/bsky/src/lexicon/types/com/atproto/server/requestPasswordReset.ts index d1973cd4825..47fb4bb62f3 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/requestPasswordReset.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/requestPasswordReset.ts @@ -26,10 +26,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/resetPassword.ts b/packages/bsky/src/lexicon/types/com/atproto/server/resetPassword.ts index 5e0148284a1..9e6ece3e4c4 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/resetPassword.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/resetPassword.ts @@ -28,10 +28,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/server/revokeAppPassword.ts b/packages/bsky/src/lexicon/types/com/atproto/server/revokeAppPassword.ts index e6bdcd09801..4627f68eaa2 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/server/revokeAppPassword.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/server/revokeAppPassword.ts @@ -26,10 +26,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/getBlob.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/getBlob.ts index 8644d8ce5d9..60750902472 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getBlob.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getBlob.ts @@ -31,10 +31,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/getBlocks.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/getBlocks.ts index 9c20c12fc29..e73410efb41 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getBlocks.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getBlocks.ts @@ -30,10 +30,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/getCheckout.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/getCheckout.ts index 5b4fd2a12d8..5de2cbfa397 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getCheckout.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getCheckout.ts @@ -31,10 +31,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/getCommitPath.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/getCommitPath.ts index cf3bd997264..4bba5b45362 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getCommitPath.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getCommitPath.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/getHead.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/getHead.ts index ce8cf66cfb4..586ae1a4189 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getHead.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getHead.ts @@ -35,10 +35,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/getRecord.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/getRecord.ts index da29fb675ff..297f0ac7794 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getRecord.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getRecord.ts @@ -33,10 +33,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/getRepo.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/getRepo.ts index 98074484cbd..d871a63ed9e 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getRepo.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getRepo.ts @@ -33,10 +33,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/listBlobs.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/listBlobs.ts index 024a703440c..49dc1a573d1 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/listBlobs.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/listBlobs.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/listRepos.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/listRepos.ts index 506328d6f60..afbc9df8475 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/listRepos.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/listRepos.ts @@ -35,13 +35,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface Repo { did: string diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/notifyOfUpdate.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/notifyOfUpdate.ts index df921fb8bc2..3d310c1139a 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/notifyOfUpdate.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/notifyOfUpdate.ts @@ -27,10 +27,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/requestCrawl.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/requestCrawl.ts index c634409f6ba..87ef20d7297 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/requestCrawl.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/requestCrawl.ts @@ -27,10 +27,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/subscribeRepos.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/subscribeRepos.ts index b0bf73edaca..9b5326ed347 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/subscribeRepos.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/subscribeRepos.ts @@ -22,12 +22,15 @@ export type OutputSchema = | { $type: string; [k: string]: unknown } export type HandlerError = ErrorFrame<'FutureCursor' | 'ConsumerTooSlow'> export type HandlerOutput = HandlerError | OutputSchema -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams req: IncomingMessage signal: AbortSignal -}) => AsyncIterable +} +export type Handler = ( + ctx: HandlerReqCtx, +) => AsyncIterable export interface Commit { seq: number diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 091e0a9bea3..2ee457837e0 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -52,6 +52,7 @@ export class TestPds { didCacheStaleTTL: HOUR, jwtSecret: 'jwt-secret', availableUserDomains: ['.test'], + rateLimitsEnabled: false, appUrlPasswordReset: 'app://forgot-password', emailNoReplyAddress: 'noreply@blueskyweb.xyz', publicUrl: 'https://pds.public.url', diff --git a/packages/lex-cli/src/codegen/server.ts b/packages/lex-cli/src/codegen/server.ts index b4e5c3759ed..c6f2d55cc78 100644 --- a/packages/lex-cli/src/codegen/server.ts +++ b/packages/lex-cli/src/codegen/server.ts @@ -182,13 +182,45 @@ const indexTs = ( ].join('\n'), ) + file.addTypeAlias({ + name: 'SharedRateLimitOpts', + typeParameters: [{ name: 'T' }], + type: `{ + name: string + calcKey?: (ctx: T) => string + calcPoints?: (ctx: T) => number + }`, + }) + + file.addTypeAlias({ + name: 'RouteRateLimitOpts', + typeParameters: [{ name: 'T' }], + type: `{ + durationMs: number + points: number + calcKey?: (ctx: T) => string + calcPoints?: (ctx: T) => number + }`, + }) + + file.addTypeAlias({ + name: 'HandlerRateLimitOpts', + typeParameters: [{ name: 'T' }], + type: `SharedRateLimitOpts | RouteRateLimitOpts`, + }) + file.addTypeAlias({ name: 'ConfigOf', - typeParameters: [{ name: 'Auth' }, { name: 'Handler' }], + typeParameters: [ + { name: 'Auth' }, + { name: 'Handler' }, + { name: 'ReqCtx' }, + ], type: ` | Handler | { auth?: Auth + rateLimit?: HandlerRateLimitOpts | HandlerRateLimitOpts[] handler: Handler }`, }) @@ -269,7 +301,7 @@ function genNamespaceCls(file: SourceFile, ns: DefTreeNode) { }) method.addParameter({ name: 'cfg', - type: `ConfigOf>>`, + type: `ConfigOf>, ${moduleName}.HandlerReqCtx>>`, }) const methodType = isSubscription ? 'streamMethod' : 'method' method.setBodyText( @@ -476,18 +508,27 @@ function genServerXrpcMethod( }) file.addTypeAlias({ - name: 'Handler', + name: 'HandlerReqCtx', isExported: true, typeParameters: [ { name: 'HA', constraint: 'HandlerAuth', default: 'never' }, ], - type: `(ctx: { + type: `{ auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response - }) => Promise | HandlerOutput`, + }`, + }) + + file.addTypeAlias({ + name: 'Handler', + isExported: true, + typeParameters: [ + { name: 'HA', constraint: 'HandlerAuth', default: 'never' }, + ], + type: `(ctx: HandlerReqCtx) => Promise | HandlerOutput`, }) } @@ -525,17 +566,26 @@ function genServerXrpcStreaming( }) file.addTypeAlias({ - name: 'Handler', + name: 'HandlerReqCtx', isExported: true, typeParameters: [ { name: 'HA', constraint: 'HandlerAuth', default: 'never' }, ], - type: `(ctx: { + type: `{ auth: HA params: QueryParams req: IncomingMessage signal: AbortSignal - }) => AsyncIterable`, + }`, + }) + + file.addTypeAlias({ + name: 'Handler', + isExported: true, + typeParameters: [ + { name: 'HA', constraint: 'HandlerAuth', default: 'never' }, + ], + type: `(ctx: HandlerReqCtx) => AsyncIterable`, }) } diff --git a/packages/pds/package.json b/packages/pds/package.json index 59b37c5de87..8442531b63c 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -54,6 +54,7 @@ "handlebars": "^4.7.7", "http-errors": "^2.0.0", "http-terminator": "^3.2.0", + "ioredis": "^5.3.2", "jsonwebtoken": "^8.5.1", "kysely": "^0.22.0", "lru-cache": "^10.0.1", diff --git a/packages/pds/src/api/com/atproto/identity/updateHandle.ts b/packages/pds/src/api/com/atproto/identity/updateHandle.ts index 17df7f576cf..ccf8e56b1bd 100644 --- a/packages/pds/src/api/com/atproto/identity/updateHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/updateHandle.ts @@ -7,10 +7,23 @@ import { UserAlreadyExistsError, } from '../../../../services/account' import { httpLogger } from '../../../../logger' +import { DAY, MINUTE } from '@atproto/common' export default function (server: Server, ctx: AppContext) { server.com.atproto.identity.updateHandle({ auth: ctx.accessVerifierCheckTakedown, + rateLimit: [ + { + durationMs: 5 * MINUTE, + points: 10, + calcKey: ({ auth }) => auth.credentials.did, + }, + { + durationMs: DAY, + points: 50, + calcKey: ({ auth }) => auth.credentials.did, + }, + ], handler: async ({ auth, input }) => { const requester = auth.credentials.did const handle = await normalizeAndValidateHandle({ diff --git a/packages/pds/src/api/com/atproto/repo/rebaseRepo.ts b/packages/pds/src/api/com/atproto/repo/rebaseRepo.ts index ec3d48ce436..e2fbffaa6d8 100644 --- a/packages/pds/src/api/com/atproto/repo/rebaseRepo.ts +++ b/packages/pds/src/api/com/atproto/repo/rebaseRepo.ts @@ -4,10 +4,16 @@ import { Server } from '../../../../lexicon' import { BadCommitSwapError } from '../../../../repo' import AppContext from '../../../../context' import { ConcurrentWriteError } from '../../../../services/repo' +import { DAY } from '@atproto/common' export default function (server: Server, ctx: AppContext) { server.com.atproto.repo.rebaseRepo({ auth: ctx.accessVerifierNotAppPassword, + rateLimit: { + durationMs: DAY, + points: 10, + calcKey: ({ auth }) => auth.credentials.did, + }, handler: async ({ input, auth }) => { const { repo, swapCommit } = input.body const did = await ctx.services.account(ctx.db).getDidForActor(repo) diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index af660d862f8..313f7ab5cd8 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -9,113 +9,120 @@ import { UserAlreadyExistsError } from '../../../../services/account' import AppContext from '../../../../context' import Database from '../../../../db' import { AtprotoData } from '@atproto/identity' +import { MINUTE } from '@atproto/common' export default function (server: Server, ctx: AppContext) { - server.com.atproto.server.createAccount(async ({ input, req }) => { - const { email, password, inviteCode } = input.body - - if (ctx.cfg.inviteRequired && !inviteCode) { - throw new InvalidRequestError( - 'No invite code provided', - 'InvalidInviteCode', - ) - } - - // normalize & ensure valid handle - const handle = await normalizeAndValidateHandle({ - ctx, - handle: input.body.handle, - did: input.body.did, - }) + server.com.atproto.server.createAccount({ + rateLimit: { + durationMs: 5 * MINUTE, + points: 100, + }, + handler: async ({ input, req }) => { + const { email, password, inviteCode } = input.body + + if (ctx.cfg.inviteRequired && !inviteCode) { + throw new InvalidRequestError( + 'No invite code provided', + 'InvalidInviteCode', + ) + } - // check that the invite code still has uses - if (ctx.cfg.inviteRequired && inviteCode) { - await ensureCodeIsAvailable(ctx.db, inviteCode) - } + // normalize & ensure valid handle + const handle = await normalizeAndValidateHandle({ + ctx, + handle: input.body.handle, + did: input.body.did, + }) - // determine the did & any plc ops we need to send - // if the provided did document is poorly setup, we throw - const { did, plcOp } = await getDidAndPlcOp(ctx, handle, input.body) + // check that the invite code still has uses + if (ctx.cfg.inviteRequired && inviteCode) { + await ensureCodeIsAvailable(ctx.db, inviteCode) + } - const now = new Date().toISOString() - const passwordScrypt = await scrypt.genSaltAndHash(password) + // determine the did & any plc ops we need to send + // if the provided did document is poorly setup, we throw + const { did, plcOp } = await getDidAndPlcOp(ctx, handle, input.body) - const result = await ctx.db.transaction(async (dbTxn) => { - const actorTxn = ctx.services.account(dbTxn) - const repoTxn = ctx.services.repo(dbTxn) + const now = new Date().toISOString() + const passwordScrypt = await scrypt.genSaltAndHash(password) - // it's a bit goofy that we run this logic twice, - // but we run it once for a sanity check before doing scrypt & plc ops - // & a second time for locking + integrity check - if (ctx.cfg.inviteRequired && inviteCode) { - await ensureCodeIsAvailable(dbTxn, inviteCode, true) - } + const result = await ctx.db.transaction(async (dbTxn) => { + const actorTxn = ctx.services.account(dbTxn) + const repoTxn = ctx.services.repo(dbTxn) - // Register user before going out to PLC to get a real did - try { - await actorTxn.registerUser({ email, handle, did, passwordScrypt }) - } catch (err) { - if (err instanceof UserAlreadyExistsError) { - const got = await actorTxn.getAccount(handle, true) - if (got) { - throw new InvalidRequestError(`Handle already taken: ${handle}`) - } else { - throw new InvalidRequestError(`Email already taken: ${email}`) - } + // it's a bit goofy that we run this logic twice, + // but we run it once for a sanity check before doing scrypt & plc ops + // & a second time for locking + integrity check + if (ctx.cfg.inviteRequired && inviteCode) { + await ensureCodeIsAvailable(dbTxn, inviteCode, true) } - throw err - } - // Generate a real did with PLC - if (plcOp) { + // Register user before going out to PLC to get a real did try { - await ctx.plcClient.sendOperation(did, plcOp) + await actorTxn.registerUser({ email, handle, did, passwordScrypt }) } catch (err) { - req.log.error( - { didKey: ctx.plcRotationKey.did(), handle }, - 'failed to create did:plc', - ) + if (err instanceof UserAlreadyExistsError) { + const got = await actorTxn.getAccount(handle, true) + if (got) { + throw new InvalidRequestError(`Handle already taken: ${handle}`) + } else { + throw new InvalidRequestError(`Email already taken: ${email}`) + } + } throw err } - } - // insert invite code use - if (ctx.cfg.inviteRequired && inviteCode) { - await dbTxn.db - .insertInto('invite_code_use') - .values({ - code: inviteCode, - usedBy: did, - usedAt: now, - }) - .execute() - } + // Generate a real did with PLC + if (plcOp) { + try { + await ctx.plcClient.sendOperation(did, plcOp) + } catch (err) { + req.log.error( + { didKey: ctx.plcRotationKey.did(), handle }, + 'failed to create did:plc', + ) + throw err + } + } - const access = ctx.auth.createAccessToken({ did }) - const refresh = ctx.auth.createRefreshToken({ did }) - await ctx.services.auth(dbTxn).grantRefreshToken(refresh.payload, null) + // insert invite code use + if (ctx.cfg.inviteRequired && inviteCode) { + await dbTxn.db + .insertInto('invite_code_use') + .values({ + code: inviteCode, + usedBy: did, + usedAt: now, + }) + .execute() + } - // Setup repo root - await repoTxn.createRepo(did, [], now) + const access = ctx.auth.createAccessToken({ did }) + const refresh = ctx.auth.createRefreshToken({ did }) + await ctx.services.auth(dbTxn).grantRefreshToken(refresh.payload, null) - return { - did, - accessJwt: access.jwt, - refreshJwt: refresh.jwt, - } - }) + // Setup repo root + await repoTxn.createRepo(did, [], now) - ctx.contentReporter?.checkHandle({ handle, did: result.did }) + return { + did, + accessJwt: access.jwt, + refreshJwt: refresh.jwt, + } + }) - return { - encoding: 'application/json', - body: { - handle, - did: result.did, - accessJwt: result.accessJwt, - refreshJwt: result.refreshJwt, - }, - } + ctx.contentReporter?.checkHandle({ handle, did: result.did }) + + return { + encoding: 'application/json', + body: { + handle, + did: result.did, + accessJwt: result.accessJwt, + refreshJwt: result.refreshJwt, + }, + } + }, }) } diff --git a/packages/pds/src/api/com/atproto/server/createSession.ts b/packages/pds/src/api/com/atproto/server/createSession.ts index 3604ded5a81..bd62a590638 100644 --- a/packages/pds/src/api/com/atproto/server/createSession.ts +++ b/packages/pds/src/api/com/atproto/server/createSession.ts @@ -3,57 +3,78 @@ import AppContext from '../../../../context' import { softDeleted } from '../../../../db/util' import { Server } from '../../../../lexicon' import { AuthScope } from '../../../../auth' +import { DAY, MINUTE } from '@atproto/common' +import { getReqIp } from '@atproto/xrpc-server/src/util' export default function (server: Server, ctx: AppContext) { - server.com.atproto.server.createSession(async ({ input }) => { - const { password } = input.body - const identifier = input.body.identifier.toLowerCase() - const authService = ctx.services.auth(ctx.db) - const actorService = ctx.services.account(ctx.db) - - const user = identifier.includes('@') - ? await actorService.getAccountByEmail(identifier, true) - : await actorService.getAccount(identifier, true) + server.com.atproto.server.createSession({ + rateLimit: [ + { + durationMs: DAY, + points: 200, + calcKey: ({ req, input }) => + `${getReqIp(req)}-${input.body.identifier}`, + }, + { + durationMs: 5 * MINUTE, + points: 10, + calcKey: ({ req, input }) => + `${getReqIp(req)}-${input.body.identifier}`, + }, + ], + handler: async ({ input }) => { + const { password } = input.body + const identifier = input.body.identifier.toLowerCase() + const authService = ctx.services.auth(ctx.db) + const actorService = ctx.services.account(ctx.db) - if (!user) { - throw new AuthRequiredError('Invalid identifier or password') - } + const user = identifier.includes('@') + ? await actorService.getAccountByEmail(identifier, true) + : await actorService.getAccount(identifier, true) - let appPasswordName: string | null = null - const validAccountPass = await actorService.verifyAccountPassword( - user.did, - password, - ) - if (!validAccountPass) { - appPasswordName = await actorService.verifyAppPassword(user.did, password) - if (appPasswordName === null) { + if (!user) { throw new AuthRequiredError('Invalid identifier or password') } - } - if (softDeleted(user)) { - throw new AuthRequiredError( - 'Account has been taken down', - 'AccountTakedown', + let appPasswordName: string | null = null + const validAccountPass = await actorService.verifyAccountPassword( + user.did, + password, ) - } + if (!validAccountPass) { + appPasswordName = await actorService.verifyAppPassword( + user.did, + password, + ) + if (appPasswordName === null) { + throw new AuthRequiredError('Invalid identifier or password') + } + } - const access = ctx.auth.createAccessToken({ - did: user.did, - scope: appPasswordName === null ? AuthScope.Access : AuthScope.AppPass, - }) - const refresh = ctx.auth.createRefreshToken({ did: user.did }) - await authService.grantRefreshToken(refresh.payload, appPasswordName) + if (softDeleted(user)) { + throw new AuthRequiredError( + 'Account has been taken down', + 'AccountTakedown', + ) + } - return { - encoding: 'application/json', - body: { + const access = ctx.auth.createAccessToken({ did: user.did, - handle: user.handle, - email: user.email, - accessJwt: access.jwt, - refreshJwt: refresh.jwt, - }, - } + scope: appPasswordName === null ? AuthScope.Access : AuthScope.AppPass, + }) + const refresh = ctx.auth.createRefreshToken({ did: user.did }) + await authService.grantRefreshToken(refresh.payload, appPasswordName) + + return { + encoding: 'application/json', + body: { + did: user.did, + handle: user.handle, + email: user.email, + accessJwt: access.jwt, + refreshJwt: refresh.jwt, + }, + } + }, }) } diff --git a/packages/pds/src/api/com/atproto/server/deleteAccount.ts b/packages/pds/src/api/com/atproto/server/deleteAccount.ts index dfdc3c47721..9ebfcfa7fdf 100644 --- a/packages/pds/src/api/com/atproto/server/deleteAccount.ts +++ b/packages/pds/src/api/com/atproto/server/deleteAccount.ts @@ -3,80 +3,91 @@ import { Server } from '../../../../lexicon' import { TAKEDOWN } from '../../../../lexicon/types/com/atproto/admin/defs' import AppContext from '../../../../context' import Database from '../../../../db' +import { MINUTE } from '@atproto/common' const REASON_ACCT_DELETION = 'ACCOUNT DELETION' export default function (server: Server, ctx: AppContext) { - server.com.atproto.server.deleteAccount(async ({ input, req }) => { - const { did, password, token } = input.body - const validPass = await ctx.services - .account(ctx.db) - .verifyAccountPassword(did, password) - if (!validPass) { - throw new AuthRequiredError('Invalid did or password') - } - - const tokenInfo = await ctx.db.db - .selectFrom('did_handle') - .innerJoin('delete_account_token as token', 'token.did', 'did_handle.did') - .where('did_handle.did', '=', did) - .where('token.token', '=', token.toUpperCase()) - .select([ - 'token.token as token', - 'token.requestedAt as requestedAt', - 'token.did as did', - ]) - .executeTakeFirst() + server.com.atproto.server.deleteAccount({ + rateLimit: { + durationMs: 5 * MINUTE, + points: 50, + }, + handler: async ({ input, req }) => { + const { did, password, token } = input.body + const validPass = await ctx.services + .account(ctx.db) + .verifyAccountPassword(did, password) + if (!validPass) { + throw new AuthRequiredError('Invalid did or password') + } - if (!tokenInfo) { - return createInvalidTokenError() - } + const tokenInfo = await ctx.db.db + .selectFrom('did_handle') + .innerJoin( + 'delete_account_token as token', + 'token.did', + 'did_handle.did', + ) + .where('did_handle.did', '=', did) + .where('token.token', '=', token.toUpperCase()) + .select([ + 'token.token as token', + 'token.requestedAt as requestedAt', + 'token.did as did', + ]) + .executeTakeFirst() - const now = new Date() - const requestedAt = new Date(tokenInfo.requestedAt) - const expiresAt = new Date(requestedAt.getTime() + 15 * minsToMs) - if (now > expiresAt) { - await removeDeleteToken(ctx.db, tokenInfo.did) - return createExpiredTokenError() - } + if (!tokenInfo) { + return createInvalidTokenError() + } - await ctx.db.transaction(async (dbTxn) => { - const moderationTxn = ctx.services.moderation(dbTxn) - const [currentAction] = await moderationTxn.getCurrentActions({ did }) - if (currentAction?.action === TAKEDOWN) { - // Do not disturb an existing takedown, continue with account deletion - return await removeDeleteToken(dbTxn, did) + const now = new Date() + const requestedAt = new Date(tokenInfo.requestedAt) + const expiresAt = new Date(requestedAt.getTime() + 15 * minsToMs) + if (now > expiresAt) { + await removeDeleteToken(ctx.db, tokenInfo.did) + return createExpiredTokenError() } - if (currentAction) { - // Reverse existing action to replace it with a self-takedown - await moderationTxn.logReverseAction({ - id: currentAction.id, + + await ctx.db.transaction(async (dbTxn) => { + const moderationTxn = ctx.services.moderation(dbTxn) + const [currentAction] = await moderationTxn.getCurrentActions({ did }) + if (currentAction?.action === TAKEDOWN) { + // Do not disturb an existing takedown, continue with account deletion + return await removeDeleteToken(dbTxn, did) + } + if (currentAction) { + // Reverse existing action to replace it with a self-takedown + await moderationTxn.logReverseAction({ + id: currentAction.id, + reason: REASON_ACCT_DELETION, + createdBy: did, + createdAt: now, + }) + } + const takedown = await moderationTxn.logAction({ + action: TAKEDOWN, + subject: { did }, reason: REASON_ACCT_DELETION, createdBy: did, createdAt: now, }) - } - const takedown = await moderationTxn.logAction({ - action: TAKEDOWN, - subject: { did }, - reason: REASON_ACCT_DELETION, - createdBy: did, - createdAt: now, + await moderationTxn.takedownRepo({ did, takedownId: takedown.id }) + await removeDeleteToken(dbTxn, did) }) - await moderationTxn.takedownRepo({ did, takedownId: takedown.id }) - await removeDeleteToken(dbTxn, did) - }) - ctx.backgroundQueue.add(async (db) => { - try { - // In the background perform the hard account deletion work - await ctx.services.record(db).deleteForActor(did) - await ctx.services.repo(db).deleteRepo(did) - await ctx.services.account(db).deleteAccount(did) - } catch (err) { - req.log.error({ did, err }, 'account deletion failed') - } - }) + ctx.backgroundQueue.add(async (db) => { + try { + // In the background perform the hard account deletion work + await ctx.services.record(db).deleteForActor(did) + await ctx.services.repo(db).deleteRepo(did) + await ctx.services.account(db).deleteAccount(did) + } catch (err) { + req.log.error({ did, err }, 'account deletion failed') + } + }) + }, }) } diff --git a/packages/pds/src/api/com/atproto/server/resetPassword.ts b/packages/pds/src/api/com/atproto/server/resetPassword.ts index 460fb144132..de8d10382c0 100644 --- a/packages/pds/src/api/com/atproto/server/resetPassword.ts +++ b/packages/pds/src/api/com/atproto/server/resetPassword.ts @@ -1,39 +1,48 @@ import AppContext from '../../../../context' import { Server } from '../../../../lexicon' import Database from '../../../../db' +import { MINUTE } from '@atproto/common' export default function (server: Server, ctx: AppContext) { - server.com.atproto.server.resetPassword(async ({ input }) => { - const { token, password } = input.body + server.com.atproto.server.resetPassword({ + rateLimit: [ + { + durationMs: 5 * MINUTE, + points: 50, + }, + ], + handler: async ({ input }) => { + const { token, password } = input.body - const tokenInfo = await ctx.db.db - .selectFrom('user_account') - .select(['did', 'passwordResetGrantedAt']) - .where('passwordResetToken', '=', token.toUpperCase()) - .executeTakeFirst() + const tokenInfo = await ctx.db.db + .selectFrom('user_account') + .select(['did', 'passwordResetGrantedAt']) + .where('passwordResetToken', '=', token.toUpperCase()) + .executeTakeFirst() - if (!tokenInfo?.passwordResetGrantedAt) { - return createInvalidTokenError() - } + if (!tokenInfo?.passwordResetGrantedAt) { + return createInvalidTokenError() + } - const now = new Date() - const grantedAt = new Date(tokenInfo.passwordResetGrantedAt) - const expiresAt = new Date(grantedAt.getTime() + 15 * minsToMs) + const now = new Date() + const grantedAt = new Date(tokenInfo.passwordResetGrantedAt) + const expiresAt = new Date(grantedAt.getTime() + 15 * minsToMs) - if (now > expiresAt) { - await unsetResetToken(ctx.db, tokenInfo.did) - return createExpiredTokenError() - } + if (now > expiresAt) { + await unsetResetToken(ctx.db, tokenInfo.did) + return createExpiredTokenError() + } - await ctx.db.transaction(async (dbTxn) => { - await unsetResetToken(dbTxn, tokenInfo.did) - await ctx.services - .account(dbTxn) - .updateUserPassword(tokenInfo.did, password) - await await ctx.services - .auth(dbTxn) - .revokeRefreshTokensByDid(tokenInfo.did) - }) + await ctx.db.transaction(async (dbTxn) => { + await unsetResetToken(dbTxn, tokenInfo.did) + await ctx.services + .account(dbTxn) + .updateUserPassword(tokenInfo.did, password) + await await ctx.services + .auth(dbTxn) + .revokeRefreshTokensByDid(tokenInfo.did) + }) + }, }) } diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index 2b8e870a1cd..741fae22fde 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -43,6 +43,11 @@ export interface ServerConfigValues { imgUriEndpoint?: string blobCacheLocation?: string + rateLimitsEnabled: boolean + rateLimitBypassKey?: string + redisScratchAddress?: string + redisScratchPassword?: string + appUrlPasswordReset: string emailSmtpUrl?: string emailNoReplyAddress: string @@ -158,6 +163,15 @@ export class ServerConfig { const imgUriEndpoint = process.env.IMG_URI_ENDPOINT const blobCacheLocation = process.env.BLOB_CACHE_LOC + const rateLimitsEnabled = process.env.RATE_LIMITS_ENABLED === 'true' + const rateLimitBypassKey = nonemptyString(process.env.RATE_LIMIT_BYPASS_KEY) + const redisScratchAddress = nonemptyString( + process.env.REDIS_SCRATCH_ADDRESS, + ) + const redisScratchPassword = nonemptyString( + process.env.REDIS_SCRATCH_PASSWORD, + ) + const appUrlPasswordReset = process.env.APP_URL_PASSWORD_RESET || 'app://password-reset' @@ -259,6 +273,10 @@ export class ServerConfig { imgUriKey, imgUriEndpoint, blobCacheLocation, + rateLimitsEnabled, + rateLimitBypassKey, + redisScratchAddress, + redisScratchPassword, appUrlPasswordReset, emailSmtpUrl, emailNoReplyAddress, @@ -439,6 +457,22 @@ export class ServerConfig { return this.cfg.blobCacheLocation } + get rateLimitsEnabled() { + return this.cfg.rateLimitsEnabled + } + + get rateLimitBypassKey() { + return this.cfg.rateLimitBypassKey + } + + get redisScratchAddress() { + return this.cfg.redisScratchAddress + } + + get redisScratchPassword() { + return this.cfg.redisScratchPassword + } + get appUrlPasswordReset() { return this.cfg.appUrlPasswordReset } diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 25697ff75b6..06740ea41e5 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -1,4 +1,5 @@ import express from 'express' +import { Redis } from 'ioredis' import * as plc from '@did-plc/lib' import * as crypto from '@atproto/crypto' import { IdResolver } from '@atproto/identity' @@ -28,6 +29,7 @@ export class AppContext { private opts: { db: Database blobstore: BlobStore + redisScratch?: Redis repoSigningKey: crypto.Keypair plcRotationKey: crypto.Keypair idResolver: IdResolver @@ -60,6 +62,10 @@ export class AppContext { return this.opts.blobstore } + get redisScratch(): Redis | undefined { + return this.opts.redisScratch + } + get repoSigningKey(): crypto.Keypair { return this.opts.repoSigningKey } diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 5409795f731..67552df8e3d 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -9,10 +9,18 @@ import cors from 'cors' import http from 'http' import events from 'events' import { createTransport } from 'nodemailer' +import { Redis } from 'ioredis' import { AtpAgent } from '@atproto/api' import * as crypto from '@atproto/crypto' import { BlobStore } from '@atproto/repo' import { IdResolver } from '@atproto/identity' +import { + RateLimiter, + RateLimiterCreator, + RateLimiterOpts, + Options as XrpcServerOptions, +} from '@atproto/xrpc-server' +import { MINUTE } from '@atproto/common' import * as appviewConsumers from './app-view/event-stream/consumers' import inProcessAppView from './app-view/api' import API from './api' @@ -46,6 +54,7 @@ import { Crawlers } from './crawlers' import { LabelCache } from './label-cache' import { ContentReporter } from './content-reporter' import { ModerationService } from './services/moderation' +import { getRedisClient } from './redis' import { RuntimeFlags } from './runtime-flags' export type { MountedAlgos } from './feed-gen/types' @@ -230,9 +239,18 @@ export class PDS { const runtimeFlags = new RuntimeFlags(db) + let redisScratch: Redis | undefined = undefined + if (config.redisScratchAddress) { + redisScratch = getRedisClient( + config.redisScratchAddress, + config.redisScratchPassword, + ) + } + const ctx = new AppContext({ db, blobstore, + redisScratch, repoSigningKey, plcRotationKey, idResolver, @@ -256,14 +274,42 @@ export class PDS { algos, }) - let server = createServer({ + const xrpcOpts: XrpcServerOptions = { validateResponse: config.debugMode, payload: { jsonLimit: 100 * 1024, // 100kb textLimit: 100 * 1024, // 100kb blobLimit: 5 * 1024 * 1024, // 5mb }, - }) + } + if (config.rateLimitsEnabled) { + let rlCreator: RateLimiterCreator + if (redisScratch) { + rlCreator = (opts: RateLimiterOpts) => + RateLimiter.redis(redisScratch, { + bypassSecret: config.rateLimitBypassKey, + ...opts, + }) + } else { + rlCreator = (opts: RateLimiterOpts) => + RateLimiter.memory({ + bypassSecret: config.rateLimitBypassKey, + ...opts, + }) + } + xrpcOpts['rateLimits'] = { + creator: rlCreator, + global: [ + { + name: 'global-ip', + durationMs: 5 * MINUTE, + points: 3000, + }, + ], + } + } + + let server = createServer(xrpcOpts) server = API(server, ctx) server = inProcessAppView(server, ctx) @@ -329,6 +375,7 @@ export class PDS { await this.terminator?.terminate() await this.ctx.backgroundQueue.destroy() await this.ctx.db.close() + await this.ctx.redisScratch?.quit() clearInterval(this.dbStatsInterval) clearInterval(this.sequencerStatsInterval) } diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 17c90d9ec8d..cd0594afef1 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -183,7 +183,8 @@ export class AdminNS { disableAccountInvites( cfg: ConfigOf< AV, - ComAtprotoAdminDisableAccountInvites.Handler> + ComAtprotoAdminDisableAccountInvites.Handler>, + ComAtprotoAdminDisableAccountInvites.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.disableAccountInvites' // @ts-ignore @@ -193,7 +194,8 @@ export class AdminNS { disableInviteCodes( cfg: ConfigOf< AV, - ComAtprotoAdminDisableInviteCodes.Handler> + ComAtprotoAdminDisableInviteCodes.Handler>, + ComAtprotoAdminDisableInviteCodes.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.disableInviteCodes' // @ts-ignore @@ -203,7 +205,8 @@ export class AdminNS { enableAccountInvites( cfg: ConfigOf< AV, - ComAtprotoAdminEnableAccountInvites.Handler> + ComAtprotoAdminEnableAccountInvites.Handler>, + ComAtprotoAdminEnableAccountInvites.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.enableAccountInvites' // @ts-ignore @@ -211,7 +214,11 @@ export class AdminNS { } getInviteCodes( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoAdminGetInviteCodes.Handler>, + ComAtprotoAdminGetInviteCodes.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.admin.getInviteCodes' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -220,7 +227,8 @@ export class AdminNS { getModerationAction( cfg: ConfigOf< AV, - ComAtprotoAdminGetModerationAction.Handler> + ComAtprotoAdminGetModerationAction.Handler>, + ComAtprotoAdminGetModerationAction.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.getModerationAction' // @ts-ignore @@ -230,7 +238,8 @@ export class AdminNS { getModerationActions( cfg: ConfigOf< AV, - ComAtprotoAdminGetModerationActions.Handler> + ComAtprotoAdminGetModerationActions.Handler>, + ComAtprotoAdminGetModerationActions.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.getModerationActions' // @ts-ignore @@ -240,7 +249,8 @@ export class AdminNS { getModerationReport( cfg: ConfigOf< AV, - ComAtprotoAdminGetModerationReport.Handler> + ComAtprotoAdminGetModerationReport.Handler>, + ComAtprotoAdminGetModerationReport.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.getModerationReport' // @ts-ignore @@ -250,7 +260,8 @@ export class AdminNS { getModerationReports( cfg: ConfigOf< AV, - ComAtprotoAdminGetModerationReports.Handler> + ComAtprotoAdminGetModerationReports.Handler>, + ComAtprotoAdminGetModerationReports.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.getModerationReports' // @ts-ignore @@ -258,21 +269,33 @@ export class AdminNS { } getRecord( - cfg: ConfigOf>>, + 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>>, + cfg: ConfigOf< + AV, + ComAtprotoAdminGetRepo.Handler>, + ComAtprotoAdminGetRepo.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.admin.getRepo' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } rebaseRepo( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoAdminRebaseRepo.Handler>, + ComAtprotoAdminRebaseRepo.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.admin.rebaseRepo' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -281,7 +304,8 @@ export class AdminNS { resolveModerationReports( cfg: ConfigOf< AV, - ComAtprotoAdminResolveModerationReports.Handler> + ComAtprotoAdminResolveModerationReports.Handler>, + ComAtprotoAdminResolveModerationReports.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.resolveModerationReports' // @ts-ignore @@ -291,7 +315,8 @@ export class AdminNS { reverseModerationAction( cfg: ConfigOf< AV, - ComAtprotoAdminReverseModerationAction.Handler> + ComAtprotoAdminReverseModerationAction.Handler>, + ComAtprotoAdminReverseModerationAction.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.reverseModerationAction' // @ts-ignore @@ -299,14 +324,22 @@ export class AdminNS { } searchRepos( - cfg: ConfigOf>>, + 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>>, + cfg: ConfigOf< + AV, + ComAtprotoAdminSendEmail.Handler>, + ComAtprotoAdminSendEmail.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.admin.sendEmail' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -315,7 +348,8 @@ export class AdminNS { takeModerationAction( cfg: ConfigOf< AV, - ComAtprotoAdminTakeModerationAction.Handler> + ComAtprotoAdminTakeModerationAction.Handler>, + ComAtprotoAdminTakeModerationAction.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.takeModerationAction' // @ts-ignore @@ -325,7 +359,8 @@ export class AdminNS { updateAccountEmail( cfg: ConfigOf< AV, - ComAtprotoAdminUpdateAccountEmail.Handler> + ComAtprotoAdminUpdateAccountEmail.Handler>, + ComAtprotoAdminUpdateAccountEmail.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.updateAccountEmail' // @ts-ignore @@ -335,7 +370,8 @@ export class AdminNS { updateAccountHandle( cfg: ConfigOf< AV, - ComAtprotoAdminUpdateAccountHandle.Handler> + ComAtprotoAdminUpdateAccountHandle.Handler>, + ComAtprotoAdminUpdateAccountHandle.HandlerReqCtx> >, ) { const nsid = 'com.atproto.admin.updateAccountHandle' // @ts-ignore @@ -351,14 +387,22 @@ export class IdentityNS { } resolveHandle( - cfg: ConfigOf>>, + 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>>, + cfg: ConfigOf< + AV, + ComAtprotoIdentityUpdateHandle.Handler>, + ComAtprotoIdentityUpdateHandle.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.identity.updateHandle' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -373,14 +417,22 @@ export class LabelNS { } queryLabels( - cfg: ConfigOf>>, + 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>>, + cfg: ConfigOf< + AV, + ComAtprotoLabelSubscribeLabels.Handler>, + ComAtprotoLabelSubscribeLabels.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.label.subscribeLabels' // @ts-ignore return this._server.xrpc.streamMethod(nsid, cfg) @@ -397,7 +449,8 @@ export class ModerationNS { createReport( cfg: ConfigOf< AV, - ComAtprotoModerationCreateReport.Handler> + ComAtprotoModerationCreateReport.Handler>, + ComAtprotoModerationCreateReport.HandlerReqCtx> >, ) { const nsid = 'com.atproto.moderation.createReport' // @ts-ignore @@ -413,63 +466,99 @@ export class RepoNS { } applyWrites( - cfg: ConfigOf>>, + 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>>, + 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>>, + 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>>, + 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>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + ComAtprotoRepoPutRecord.Handler>, + ComAtprotoRepoPutRecord.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.repo.putRecord' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } rebaseRepo( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoRepoRebaseRepo.Handler>, + ComAtprotoRepoRebaseRepo.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.repo.rebaseRepo' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } uploadBlob( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoRepoUploadBlob.Handler>, + ComAtprotoRepoUploadBlob.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.repo.uploadBlob' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -484,7 +573,11 @@ export class ServerNS { } createAccount( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoServerCreateAccount.Handler>, + ComAtprotoServerCreateAccount.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.server.createAccount' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -493,7 +586,8 @@ export class ServerNS { createAppPassword( cfg: ConfigOf< AV, - ComAtprotoServerCreateAppPassword.Handler> + ComAtprotoServerCreateAppPassword.Handler>, + ComAtprotoServerCreateAppPassword.HandlerReqCtx> >, ) { const nsid = 'com.atproto.server.createAppPassword' // @ts-ignore @@ -503,7 +597,8 @@ export class ServerNS { createInviteCode( cfg: ConfigOf< AV, - ComAtprotoServerCreateInviteCode.Handler> + ComAtprotoServerCreateInviteCode.Handler>, + ComAtprotoServerCreateInviteCode.HandlerReqCtx> >, ) { const nsid = 'com.atproto.server.createInviteCode' // @ts-ignore @@ -513,7 +608,8 @@ export class ServerNS { createInviteCodes( cfg: ConfigOf< AV, - ComAtprotoServerCreateInviteCodes.Handler> + ComAtprotoServerCreateInviteCodes.Handler>, + ComAtprotoServerCreateInviteCodes.HandlerReqCtx> >, ) { const nsid = 'com.atproto.server.createInviteCodes' // @ts-ignore @@ -521,28 +617,44 @@ export class ServerNS { } createSession( - cfg: ConfigOf>>, + 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>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + ComAtprotoServerDescribeServer.Handler>, + ComAtprotoServerDescribeServer.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.server.describeServer' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -551,7 +663,8 @@ export class ServerNS { getAccountInviteCodes( cfg: ConfigOf< AV, - ComAtprotoServerGetAccountInviteCodes.Handler> + ComAtprotoServerGetAccountInviteCodes.Handler>, + ComAtprotoServerGetAccountInviteCodes.HandlerReqCtx> >, ) { const nsid = 'com.atproto.server.getAccountInviteCodes' // @ts-ignore @@ -559,7 +672,11 @@ export class ServerNS { } getSession( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoServerGetSession.Handler>, + ComAtprotoServerGetSession.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.server.getSession' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -568,7 +685,8 @@ export class ServerNS { listAppPasswords( cfg: ConfigOf< AV, - ComAtprotoServerListAppPasswords.Handler> + ComAtprotoServerListAppPasswords.Handler>, + ComAtprotoServerListAppPasswords.HandlerReqCtx> >, ) { const nsid = 'com.atproto.server.listAppPasswords' // @ts-ignore @@ -576,7 +694,11 @@ export class ServerNS { } refreshSession( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoServerRefreshSession.Handler>, + ComAtprotoServerRefreshSession.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.server.refreshSession' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -585,7 +707,8 @@ export class ServerNS { requestAccountDelete( cfg: ConfigOf< AV, - ComAtprotoServerRequestAccountDelete.Handler> + ComAtprotoServerRequestAccountDelete.Handler>, + ComAtprotoServerRequestAccountDelete.HandlerReqCtx> >, ) { const nsid = 'com.atproto.server.requestAccountDelete' // @ts-ignore @@ -595,7 +718,8 @@ export class ServerNS { requestPasswordReset( cfg: ConfigOf< AV, - ComAtprotoServerRequestPasswordReset.Handler> + ComAtprotoServerRequestPasswordReset.Handler>, + ComAtprotoServerRequestPasswordReset.HandlerReqCtx> >, ) { const nsid = 'com.atproto.server.requestPasswordReset' // @ts-ignore @@ -603,7 +727,11 @@ export class ServerNS { } resetPassword( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoServerResetPassword.Handler>, + ComAtprotoServerResetPassword.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.server.resetPassword' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -612,7 +740,8 @@ export class ServerNS { revokeAppPassword( cfg: ConfigOf< AV, - ComAtprotoServerRevokeAppPassword.Handler> + ComAtprotoServerRevokeAppPassword.Handler>, + ComAtprotoServerRevokeAppPassword.HandlerReqCtx> >, ) { const nsid = 'com.atproto.server.revokeAppPassword' // @ts-ignore @@ -628,84 +757,132 @@ export class SyncNS { } getBlob( - cfg: ConfigOf>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + ComAtprotoSyncGetCheckout.Handler>, + ComAtprotoSyncGetCheckout.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.sync.getCheckout' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } getCommitPath( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoSyncGetCommitPath.Handler>, + ComAtprotoSyncGetCommitPath.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.sync.getCommitPath' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } getHead( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + ComAtprotoSyncGetHead.Handler>, + ComAtprotoSyncGetHead.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.sync.getHead' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } getRecord( - cfg: ConfigOf>>, + 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>>, + 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>>, + 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>>, + 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>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + ComAtprotoSyncSubscribeRepos.Handler>, + ComAtprotoSyncSubscribeRepos.HandlerReqCtx> + >, ) { const nsid = 'com.atproto.sync.subscribeRepos' // @ts-ignore return this._server.xrpc.streamMethod(nsid, cfg) @@ -752,42 +929,66 @@ export class ActorNS { } getPreferences( - cfg: ConfigOf>>, + 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>>, + 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>>, + 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>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + AppBskyActorSearchActors.Handler>, + AppBskyActorSearchActors.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.actor.searchActors' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -796,7 +997,8 @@ export class ActorNS { searchActorsTypeahead( cfg: ConfigOf< AV, - AppBskyActorSearchActorsTypeahead.Handler> + AppBskyActorSearchActorsTypeahead.Handler>, + AppBskyActorSearchActorsTypeahead.HandlerReqCtx> >, ) { const nsid = 'app.bsky.actor.searchActorsTypeahead' // @ts-ignore @@ -822,7 +1024,8 @@ export class FeedNS { describeFeedGenerator( cfg: ConfigOf< AV, - AppBskyFeedDescribeFeedGenerator.Handler> + AppBskyFeedDescribeFeedGenerator.Handler>, + AppBskyFeedDescribeFeedGenerator.HandlerReqCtx> >, ) { const nsid = 'app.bsky.feed.describeFeedGenerator' // @ts-ignore @@ -830,77 +1033,121 @@ export class FeedNS { } getActorFeeds( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + AppBskyFeedGetActorFeeds.Handler>, + AppBskyFeedGetActorFeeds.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.feed.getActorFeeds' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } getAuthorFeed( - cfg: ConfigOf>>, + 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>>, + 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>>, + 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>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + AppBskyFeedGetLikes.Handler>, + AppBskyFeedGetLikes.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.feed.getLikes' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } getPostThread( - cfg: ConfigOf>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + AppBskyFeedGetRepostedBy.Handler>, + AppBskyFeedGetRepostedBy.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.feed.getRepostedBy' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } getTimeline( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + AppBskyFeedGetTimeline.Handler>, + AppBskyFeedGetTimeline.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.feed.getTimeline' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -915,77 +1162,121 @@ export class GraphNS { } getBlocks( - cfg: ConfigOf>>, + 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>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + AppBskyGraphGetList.Handler>, + AppBskyGraphGetList.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.graph.getList' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } getListMutes( - cfg: ConfigOf>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + AppBskyGraphGetMutes.Handler>, + AppBskyGraphGetMutes.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.graph.getMutes' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } muteActor( - cfg: ConfigOf>>, + 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>>, + 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>>, + 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>>, + cfg: ConfigOf< + AV, + AppBskyGraphUnmuteActorList.Handler>, + AppBskyGraphUnmuteActorList.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.graph.unmuteActorList' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -1002,7 +1293,8 @@ export class NotificationNS { getUnreadCount( cfg: ConfigOf< AV, - AppBskyNotificationGetUnreadCount.Handler> + AppBskyNotificationGetUnreadCount.Handler>, + AppBskyNotificationGetUnreadCount.HandlerReqCtx> >, ) { const nsid = 'app.bsky.notification.getUnreadCount' // @ts-ignore @@ -1012,7 +1304,8 @@ export class NotificationNS { listNotifications( cfg: ConfigOf< AV, - AppBskyNotificationListNotifications.Handler> + AppBskyNotificationListNotifications.Handler>, + AppBskyNotificationListNotifications.HandlerReqCtx> >, ) { const nsid = 'app.bsky.notification.listNotifications' // @ts-ignore @@ -1020,7 +1313,11 @@ export class NotificationNS { } updateSeen( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + AppBskyNotificationUpdateSeen.Handler>, + AppBskyNotificationUpdateSeen.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.notification.updateSeen' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -1043,14 +1340,22 @@ export class UnspeccedNS { } applyLabels( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + AppBskyUnspeccedApplyLabels.Handler>, + AppBskyUnspeccedApplyLabels.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.unspecced.applyLabels' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } getPopular( - cfg: ConfigOf>>, + cfg: ConfigOf< + AV, + AppBskyUnspeccedGetPopular.Handler>, + AppBskyUnspeccedGetPopular.HandlerReqCtx> + >, ) { const nsid = 'app.bsky.unspecced.getPopular' // @ts-ignore return this._server.xrpc.method(nsid, cfg) @@ -1059,7 +1364,8 @@ export class UnspeccedNS { getPopularFeedGenerators( cfg: ConfigOf< AV, - AppBskyUnspeccedGetPopularFeedGenerators.Handler> + AppBskyUnspeccedGetPopularFeedGenerators.Handler>, + AppBskyUnspeccedGetPopularFeedGenerators.HandlerReqCtx> >, ) { const nsid = 'app.bsky.unspecced.getPopularFeedGenerators' // @ts-ignore @@ -1069,7 +1375,8 @@ export class UnspeccedNS { getTimelineSkeleton( cfg: ConfigOf< AV, - AppBskyUnspeccedGetTimelineSkeleton.Handler> + AppBskyUnspeccedGetTimelineSkeleton.Handler>, + AppBskyUnspeccedGetTimelineSkeleton.HandlerReqCtx> >, ) { const nsid = 'app.bsky.unspecced.getTimelineSkeleton' // @ts-ignore @@ -1077,10 +1384,23 @@ export class UnspeccedNS { } } -type ConfigOf = +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 HandlerRateLimitOpts = SharedRateLimitOpts | RouteRateLimitOpts +type ConfigOf = | Handler | { auth?: Auth + rateLimit?: HandlerRateLimitOpts | HandlerRateLimitOpts[] handler: Handler } type ExtractAuth = Extract< diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/getPreferences.ts b/packages/pds/src/lexicon/types/app/bsky/actor/getPreferences.ts index 9ba813a4313..88d78a57cba 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/getPreferences.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/getPreferences.ts @@ -32,10 +32,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/getProfile.ts b/packages/pds/src/lexicon/types/app/bsky/actor/getProfile.ts index 4a2ef252e7e..802afda5361 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/getProfile.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/getProfile.ts @@ -29,10 +29,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/getProfiles.ts b/packages/pds/src/lexicon/types/app/bsky/actor/getProfiles.ts index 89c014ebe0a..2549b264e33 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/getProfiles.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/getProfiles.ts @@ -34,10 +34,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/getSuggestions.ts b/packages/pds/src/lexicon/types/app/bsky/actor/getSuggestions.ts index 5e59cf571d5..a6d4d6102af 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/getSuggestions.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/getSuggestions.ts @@ -36,10 +36,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/putPreferences.ts b/packages/pds/src/lexicon/types/app/bsky/actor/putPreferences.ts index ba0531cc3c0..1e5ee2d834e 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/putPreferences.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/putPreferences.ts @@ -27,10 +27,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/searchActors.ts b/packages/pds/src/lexicon/types/app/bsky/actor/searchActors.ts index 0b425ba4df7..f620a463cff 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/searchActors.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/searchActors.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts b/packages/pds/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts index 43a6be287b4..4f5bbb7c23c 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/searchActorsTypeahead.ts @@ -35,10 +35,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts b/packages/pds/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts index 9444257c905..d329bf20a5a 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/describeFeedGenerator.ts @@ -33,13 +33,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface Feed { uri: string diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getActorFeeds.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getActorFeeds.ts index 2a1e9edb889..3e930cbe201 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getActorFeeds.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getActorFeeds.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts index 8c44be045ac..cd66ef5c392 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getAuthorFeed.ts @@ -43,10 +43,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getFeed.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getFeed.ts index 837a6d9a892..e72b1010aea 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getFeed.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getFeed.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts index 859f0c70b84..fab3b30c316 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getFeedGenerator.ts @@ -36,10 +36,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getFeedGenerators.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getFeedGenerators.ts index 85cbb544721..d7e082f2362 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getFeedGenerators.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getFeedGenerators.ts @@ -34,10 +34,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts index fe132e926f6..1c8f349b42b 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getFeedSkeleton.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getLikes.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getLikes.ts index deb95fb063a..d581f5bfa9c 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getLikes.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getLikes.ts @@ -40,13 +40,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface Like { indexedAt: string diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getPostThread.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getPostThread.ts index e9f154a86a6..61de94b729d 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getPostThread.ts @@ -41,10 +41,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getPosts.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getPosts.ts index 3cf5747ec1f..4282f5d349f 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getPosts.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getPosts.ts @@ -34,10 +34,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getRepostedBy.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getRepostedBy.ts index 7f7d51a77af..0b9c1a6f68b 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getRepostedBy.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getRepostedBy.ts @@ -40,10 +40,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getTimeline.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getTimeline.ts index 6be4293374c..832caf5c6f7 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getTimeline.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getTimeline.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/graph/getBlocks.ts b/packages/pds/src/lexicon/types/app/bsky/graph/getBlocks.ts index f3a6eb4b55c..d380a14880a 100644 --- a/packages/pds/src/lexicon/types/app/bsky/graph/getBlocks.ts +++ b/packages/pds/src/lexicon/types/app/bsky/graph/getBlocks.ts @@ -36,10 +36,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/graph/getFollowers.ts b/packages/pds/src/lexicon/types/app/bsky/graph/getFollowers.ts index e6bec023938..b337be52c1b 100644 --- a/packages/pds/src/lexicon/types/app/bsky/graph/getFollowers.ts +++ b/packages/pds/src/lexicon/types/app/bsky/graph/getFollowers.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/graph/getFollows.ts b/packages/pds/src/lexicon/types/app/bsky/graph/getFollows.ts index 96c18201e1b..71e9ca0270c 100644 --- a/packages/pds/src/lexicon/types/app/bsky/graph/getFollows.ts +++ b/packages/pds/src/lexicon/types/app/bsky/graph/getFollows.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/graph/getList.ts b/packages/pds/src/lexicon/types/app/bsky/graph/getList.ts index d0cfe398adf..fc45dd20985 100644 --- a/packages/pds/src/lexicon/types/app/bsky/graph/getList.ts +++ b/packages/pds/src/lexicon/types/app/bsky/graph/getList.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/graph/getListMutes.ts b/packages/pds/src/lexicon/types/app/bsky/graph/getListMutes.ts index b0820a7ea53..04cca70b44d 100644 --- a/packages/pds/src/lexicon/types/app/bsky/graph/getListMutes.ts +++ b/packages/pds/src/lexicon/types/app/bsky/graph/getListMutes.ts @@ -36,10 +36,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/graph/getLists.ts b/packages/pds/src/lexicon/types/app/bsky/graph/getLists.ts index b6626cbe096..8acf9362c00 100644 --- a/packages/pds/src/lexicon/types/app/bsky/graph/getLists.ts +++ b/packages/pds/src/lexicon/types/app/bsky/graph/getLists.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/graph/getMutes.ts b/packages/pds/src/lexicon/types/app/bsky/graph/getMutes.ts index 32956284d7d..0034095b975 100644 --- a/packages/pds/src/lexicon/types/app/bsky/graph/getMutes.ts +++ b/packages/pds/src/lexicon/types/app/bsky/graph/getMutes.ts @@ -36,10 +36,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/graph/muteActor.ts b/packages/pds/src/lexicon/types/app/bsky/graph/muteActor.ts index fd19f661533..52d1b864989 100644 --- a/packages/pds/src/lexicon/types/app/bsky/graph/muteActor.ts +++ b/packages/pds/src/lexicon/types/app/bsky/graph/muteActor.ts @@ -26,10 +26,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/graph/muteActorList.ts b/packages/pds/src/lexicon/types/app/bsky/graph/muteActorList.ts index e099011b3f7..bf803f388af 100644 --- a/packages/pds/src/lexicon/types/app/bsky/graph/muteActorList.ts +++ b/packages/pds/src/lexicon/types/app/bsky/graph/muteActorList.ts @@ -26,10 +26,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/graph/unmuteActor.ts b/packages/pds/src/lexicon/types/app/bsky/graph/unmuteActor.ts index fd19f661533..52d1b864989 100644 --- a/packages/pds/src/lexicon/types/app/bsky/graph/unmuteActor.ts +++ b/packages/pds/src/lexicon/types/app/bsky/graph/unmuteActor.ts @@ -26,10 +26,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/graph/unmuteActorList.ts b/packages/pds/src/lexicon/types/app/bsky/graph/unmuteActorList.ts index e099011b3f7..bf803f388af 100644 --- a/packages/pds/src/lexicon/types/app/bsky/graph/unmuteActorList.ts +++ b/packages/pds/src/lexicon/types/app/bsky/graph/unmuteActorList.ts @@ -26,10 +26,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/notification/getUnreadCount.ts b/packages/pds/src/lexicon/types/app/bsky/notification/getUnreadCount.ts index 365becb061b..6cf3c84beb5 100644 --- a/packages/pds/src/lexicon/types/app/bsky/notification/getUnreadCount.ts +++ b/packages/pds/src/lexicon/types/app/bsky/notification/getUnreadCount.ts @@ -33,10 +33,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/notification/listNotifications.ts b/packages/pds/src/lexicon/types/app/bsky/notification/listNotifications.ts index 02df7ade82e..156ba349ec4 100644 --- a/packages/pds/src/lexicon/types/app/bsky/notification/listNotifications.ts +++ b/packages/pds/src/lexicon/types/app/bsky/notification/listNotifications.ts @@ -38,13 +38,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface Notification { uri: string diff --git a/packages/pds/src/lexicon/types/app/bsky/notification/updateSeen.ts b/packages/pds/src/lexicon/types/app/bsky/notification/updateSeen.ts index 177f1f4bdbc..136191edc40 100644 --- a/packages/pds/src/lexicon/types/app/bsky/notification/updateSeen.ts +++ b/packages/pds/src/lexicon/types/app/bsky/notification/updateSeen.ts @@ -26,10 +26,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/unspecced/applyLabels.ts b/packages/pds/src/lexicon/types/app/bsky/unspecced/applyLabels.ts index 1c07d41d040..1d359a9547d 100644 --- a/packages/pds/src/lexicon/types/app/bsky/unspecced/applyLabels.ts +++ b/packages/pds/src/lexicon/types/app/bsky/unspecced/applyLabels.ts @@ -27,10 +27,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/unspecced/getPopular.ts b/packages/pds/src/lexicon/types/app/bsky/unspecced/getPopular.ts index 44cfe695920..8471ed77a6c 100644 --- a/packages/pds/src/lexicon/types/app/bsky/unspecced/getPopular.ts +++ b/packages/pds/src/lexicon/types/app/bsky/unspecced/getPopular.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/pds/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts index 58f385b958e..97937e926c2 100644 --- a/packages/pds/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/pds/src/lexicon/types/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/pds/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts index 1178a844a93..4ccad20c902 100644 --- a/packages/pds/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts +++ b/packages/pds/src/lexicon/types/app/bsky/unspecced/getTimelineSkeleton.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts b/packages/pds/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts index 47b3e6a2f55..051fabb65e1 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/disableAccountInvites.ts @@ -28,10 +28,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/disableInviteCodes.ts b/packages/pds/src/lexicon/types/com/atproto/admin/disableInviteCodes.ts index 2e9d326afe9..2b64371f1ed 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/disableInviteCodes.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/disableInviteCodes.ts @@ -27,10 +27,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts b/packages/pds/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts index 7f437adfb44..4a26d302333 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/enableAccountInvites.ts @@ -28,10 +28,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/getInviteCodes.ts b/packages/pds/src/lexicon/types/com/atproto/admin/getInviteCodes.ts index e3f3fb6e739..1eb099aae66 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/getInviteCodes.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/getInviteCodes.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/getModerationAction.ts b/packages/pds/src/lexicon/types/com/atproto/admin/getModerationAction.ts index f774416a083..2ab52f237cc 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/getModerationAction.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/getModerationAction.ts @@ -29,10 +29,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/getModerationActions.ts b/packages/pds/src/lexicon/types/com/atproto/admin/getModerationActions.ts index b8a1e683ec5..4c29f965df6 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/getModerationActions.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/getModerationActions.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/getModerationReport.ts b/packages/pds/src/lexicon/types/com/atproto/admin/getModerationReport.ts index 63a1a551f97..28d714453f2 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/getModerationReport.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/getModerationReport.ts @@ -29,10 +29,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/getModerationReports.ts b/packages/pds/src/lexicon/types/com/atproto/admin/getModerationReports.ts index f3372b96cc8..d50af44c757 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/getModerationReports.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/getModerationReports.ts @@ -51,10 +51,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/getRecord.ts b/packages/pds/src/lexicon/types/com/atproto/admin/getRecord.ts index 77895570d96..48222d9d819 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/getRecord.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/getRecord.ts @@ -31,10 +31,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/getRepo.ts b/packages/pds/src/lexicon/types/com/atproto/admin/getRepo.ts index 90aca8bdce5..19911baa90a 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/getRepo.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/getRepo.ts @@ -30,10 +30,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/rebaseRepo.ts b/packages/pds/src/lexicon/types/com/atproto/admin/rebaseRepo.ts index d94a817f47c..d2fe04b1386 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/rebaseRepo.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/rebaseRepo.ts @@ -30,10 +30,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/resolveModerationReports.ts b/packages/pds/src/lexicon/types/com/atproto/admin/resolveModerationReports.ts index b8558d4e2b7..e3f4d028202 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/resolveModerationReports.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/resolveModerationReports.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/reverseModerationAction.ts b/packages/pds/src/lexicon/types/com/atproto/admin/reverseModerationAction.ts index 8f17d275761..17dcb5085de 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/reverseModerationAction.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/reverseModerationAction.ts @@ -37,10 +37,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/searchRepos.ts b/packages/pds/src/lexicon/types/com/atproto/admin/searchRepos.ts index 4dc0570276b..c79cd046ca0 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/searchRepos.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/searchRepos.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/sendEmail.ts b/packages/pds/src/lexicon/types/com/atproto/admin/sendEmail.ts index 356d4513d1a..87e7ceec172 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/sendEmail.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/sendEmail.ts @@ -39,10 +39,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/takeModerationAction.ts b/packages/pds/src/lexicon/types/com/atproto/admin/takeModerationAction.ts index c2cab3f1928..fbbf14dff0f 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/takeModerationAction.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/takeModerationAction.ts @@ -52,10 +52,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts b/packages/pds/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts index eb00af00727..9e6140256ef 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/updateAccountEmail.ts @@ -28,10 +28,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/updateAccountHandle.ts b/packages/pds/src/lexicon/types/com/atproto/admin/updateAccountHandle.ts index b9bb76a9c06..c378f421926 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/updateAccountHandle.ts +++ b/packages/pds/src/lexicon/types/com/atproto/admin/updateAccountHandle.ts @@ -27,10 +27,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/identity/resolveHandle.ts b/packages/pds/src/lexicon/types/com/atproto/identity/resolveHandle.ts index bdcd0ac45d9..ef90e99bb30 100644 --- a/packages/pds/src/lexicon/types/com/atproto/identity/resolveHandle.ts +++ b/packages/pds/src/lexicon/types/com/atproto/identity/resolveHandle.ts @@ -34,10 +34,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/identity/updateHandle.ts b/packages/pds/src/lexicon/types/com/atproto/identity/updateHandle.ts index c0c3cf523c5..1f639c344e9 100644 --- a/packages/pds/src/lexicon/types/com/atproto/identity/updateHandle.ts +++ b/packages/pds/src/lexicon/types/com/atproto/identity/updateHandle.ts @@ -26,10 +26,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/label/queryLabels.ts b/packages/pds/src/lexicon/types/com/atproto/label/queryLabels.ts index 791e474c50b..72cf5c52be6 100644 --- a/packages/pds/src/lexicon/types/com/atproto/label/queryLabels.ts +++ b/packages/pds/src/lexicon/types/com/atproto/label/queryLabels.ts @@ -40,10 +40,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/label/subscribeLabels.ts b/packages/pds/src/lexicon/types/com/atproto/label/subscribeLabels.ts index 758407e0437..9d4b4441ae0 100644 --- a/packages/pds/src/lexicon/types/com/atproto/label/subscribeLabels.ts +++ b/packages/pds/src/lexicon/types/com/atproto/label/subscribeLabels.ts @@ -20,12 +20,15 @@ export type OutputSchema = | { $type: string; [k: string]: unknown } export type HandlerError = ErrorFrame<'FutureCursor'> export type HandlerOutput = HandlerError | OutputSchema -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams req: IncomingMessage signal: AbortSignal -}) => AsyncIterable +} +export type Handler = ( + ctx: HandlerReqCtx, +) => AsyncIterable export interface Labels { seq: number diff --git a/packages/pds/src/lexicon/types/com/atproto/moderation/createReport.ts b/packages/pds/src/lexicon/types/com/atproto/moderation/createReport.ts index a1be0464bf3..96aaf4a9c29 100644 --- a/packages/pds/src/lexicon/types/com/atproto/moderation/createReport.ts +++ b/packages/pds/src/lexicon/types/com/atproto/moderation/createReport.ts @@ -53,10 +53,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/repo/applyWrites.ts b/packages/pds/src/lexicon/types/com/atproto/repo/applyWrites.ts index 6fba024d715..53f2972e116 100644 --- a/packages/pds/src/lexicon/types/com/atproto/repo/applyWrites.ts +++ b/packages/pds/src/lexicon/types/com/atproto/repo/applyWrites.ts @@ -32,13 +32,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput /** Create a new record. */ export interface Create { diff --git a/packages/pds/src/lexicon/types/com/atproto/repo/createRecord.ts b/packages/pds/src/lexicon/types/com/atproto/repo/createRecord.ts index f731dee536a..e069f8caf74 100644 --- a/packages/pds/src/lexicon/types/com/atproto/repo/createRecord.ts +++ b/packages/pds/src/lexicon/types/com/atproto/repo/createRecord.ts @@ -50,10 +50,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/repo/deleteRecord.ts b/packages/pds/src/lexicon/types/com/atproto/repo/deleteRecord.ts index 016547e4075..5ee016cbed1 100644 --- a/packages/pds/src/lexicon/types/com/atproto/repo/deleteRecord.ts +++ b/packages/pds/src/lexicon/types/com/atproto/repo/deleteRecord.ts @@ -36,10 +36,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/repo/describeRepo.ts b/packages/pds/src/lexicon/types/com/atproto/repo/describeRepo.ts index 6c1300daba4..7b8a2b995eb 100644 --- a/packages/pds/src/lexicon/types/com/atproto/repo/describeRepo.ts +++ b/packages/pds/src/lexicon/types/com/atproto/repo/describeRepo.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/repo/getRecord.ts b/packages/pds/src/lexicon/types/com/atproto/repo/getRecord.ts index ecabf517539..35c9b4b7166 100644 --- a/packages/pds/src/lexicon/types/com/atproto/repo/getRecord.ts +++ b/packages/pds/src/lexicon/types/com/atproto/repo/getRecord.ts @@ -42,10 +42,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/repo/listRecords.ts b/packages/pds/src/lexicon/types/com/atproto/repo/listRecords.ts index 76c3429833c..e58d9714e33 100644 --- a/packages/pds/src/lexicon/types/com/atproto/repo/listRecords.ts +++ b/packages/pds/src/lexicon/types/com/atproto/repo/listRecords.ts @@ -46,13 +46,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface Record { uri: string diff --git a/packages/pds/src/lexicon/types/com/atproto/repo/putRecord.ts b/packages/pds/src/lexicon/types/com/atproto/repo/putRecord.ts index a56b6ca25f1..364eb59f6f1 100644 --- a/packages/pds/src/lexicon/types/com/atproto/repo/putRecord.ts +++ b/packages/pds/src/lexicon/types/com/atproto/repo/putRecord.ts @@ -52,10 +52,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/repo/rebaseRepo.ts b/packages/pds/src/lexicon/types/com/atproto/repo/rebaseRepo.ts index d94a817f47c..d2fe04b1386 100644 --- a/packages/pds/src/lexicon/types/com/atproto/repo/rebaseRepo.ts +++ b/packages/pds/src/lexicon/types/com/atproto/repo/rebaseRepo.ts @@ -30,10 +30,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/repo/uploadBlob.ts b/packages/pds/src/lexicon/types/com/atproto/repo/uploadBlob.ts index b3f06f30f39..ad6002df925 100644 --- a/packages/pds/src/lexicon/types/com/atproto/repo/uploadBlob.ts +++ b/packages/pds/src/lexicon/types/com/atproto/repo/uploadBlob.ts @@ -35,10 +35,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/server/createAccount.ts b/packages/pds/src/lexicon/types/com/atproto/server/createAccount.ts index 646091fde1e..c67e7445bf9 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/createAccount.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/createAccount.ts @@ -53,10 +53,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/server/createAppPassword.ts b/packages/pds/src/lexicon/types/com/atproto/server/createAppPassword.ts index 39eb73140c6..8e4a0a519e0 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/createAppPassword.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/createAppPassword.ts @@ -35,13 +35,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface AppPassword { name: string diff --git a/packages/pds/src/lexicon/types/com/atproto/server/createInviteCode.ts b/packages/pds/src/lexicon/types/com/atproto/server/createInviteCode.ts index 10fef7ca0e4..acfac56ba76 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/createInviteCode.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/createInviteCode.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/server/createInviteCodes.ts b/packages/pds/src/lexicon/types/com/atproto/server/createInviteCodes.ts index 98d6585ea06..5887d77fada 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/createInviteCodes.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/createInviteCodes.ts @@ -39,13 +39,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface AccountCodes { account: string diff --git a/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts b/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts index 1b184da071f..b836551f301 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/createSession.ts @@ -44,10 +44,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/server/deleteAccount.ts b/packages/pds/src/lexicon/types/com/atproto/server/deleteAccount.ts index 6fde5f17e90..37ddbba13e0 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/deleteAccount.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/deleteAccount.ts @@ -29,10 +29,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/server/deleteSession.ts b/packages/pds/src/lexicon/types/com/atproto/server/deleteSession.ts index 909c2be8cab..e4244870425 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/deleteSession.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/deleteSession.ts @@ -19,10 +19,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/server/describeServer.ts b/packages/pds/src/lexicon/types/com/atproto/server/describeServer.ts index 9d65f932ec2..bc73d541a04 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/describeServer.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/describeServer.ts @@ -33,13 +33,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface Links { privacyPolicy?: string diff --git a/packages/pds/src/lexicon/types/com/atproto/server/getAccountInviteCodes.ts b/packages/pds/src/lexicon/types/com/atproto/server/getAccountInviteCodes.ts index d0a9dc34307..e387a5e38e4 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/getAccountInviteCodes.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/getAccountInviteCodes.ts @@ -36,10 +36,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/server/getSession.ts b/packages/pds/src/lexicon/types/com/atproto/server/getSession.ts index c0f868ca0c1..388fb5eae9d 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/getSession.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/getSession.ts @@ -33,10 +33,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/server/listAppPasswords.ts b/packages/pds/src/lexicon/types/com/atproto/server/listAppPasswords.ts index ca0d4e80839..ebd74da9d39 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/listAppPasswords.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/listAppPasswords.ts @@ -32,13 +32,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface AppPassword { name: string diff --git a/packages/pds/src/lexicon/types/com/atproto/server/refreshSession.ts b/packages/pds/src/lexicon/types/com/atproto/server/refreshSession.ts index 56e34eaa71f..e47bf09fbc2 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/refreshSession.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/refreshSession.ts @@ -35,10 +35,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/server/requestAccountDelete.ts b/packages/pds/src/lexicon/types/com/atproto/server/requestAccountDelete.ts index 909c2be8cab..e4244870425 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/requestAccountDelete.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/requestAccountDelete.ts @@ -19,10 +19,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/server/requestPasswordReset.ts b/packages/pds/src/lexicon/types/com/atproto/server/requestPasswordReset.ts index d1973cd4825..47fb4bb62f3 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/requestPasswordReset.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/requestPasswordReset.ts @@ -26,10 +26,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/server/resetPassword.ts b/packages/pds/src/lexicon/types/com/atproto/server/resetPassword.ts index 5e0148284a1..9e6ece3e4c4 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/resetPassword.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/resetPassword.ts @@ -28,10 +28,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/server/revokeAppPassword.ts b/packages/pds/src/lexicon/types/com/atproto/server/revokeAppPassword.ts index e6bdcd09801..4627f68eaa2 100644 --- a/packages/pds/src/lexicon/types/com/atproto/server/revokeAppPassword.ts +++ b/packages/pds/src/lexicon/types/com/atproto/server/revokeAppPassword.ts @@ -26,10 +26,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/getBlob.ts b/packages/pds/src/lexicon/types/com/atproto/sync/getBlob.ts index 8644d8ce5d9..60750902472 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/getBlob.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/getBlob.ts @@ -31,10 +31,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/getBlocks.ts b/packages/pds/src/lexicon/types/com/atproto/sync/getBlocks.ts index 9c20c12fc29..e73410efb41 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/getBlocks.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/getBlocks.ts @@ -30,10 +30,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/getCheckout.ts b/packages/pds/src/lexicon/types/com/atproto/sync/getCheckout.ts index 5b4fd2a12d8..5de2cbfa397 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/getCheckout.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/getCheckout.ts @@ -31,10 +31,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/getCommitPath.ts b/packages/pds/src/lexicon/types/com/atproto/sync/getCommitPath.ts index cf3bd997264..4bba5b45362 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/getCommitPath.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/getCommitPath.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/getHead.ts b/packages/pds/src/lexicon/types/com/atproto/sync/getHead.ts index ce8cf66cfb4..586ae1a4189 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/getHead.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/getHead.ts @@ -35,10 +35,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/getRecord.ts b/packages/pds/src/lexicon/types/com/atproto/sync/getRecord.ts index da29fb675ff..297f0ac7794 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/getRecord.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/getRecord.ts @@ -33,10 +33,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/getRepo.ts b/packages/pds/src/lexicon/types/com/atproto/sync/getRepo.ts index 98074484cbd..d871a63ed9e 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/getRepo.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/getRepo.ts @@ -33,10 +33,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/listBlobs.ts b/packages/pds/src/lexicon/types/com/atproto/sync/listBlobs.ts index 024a703440c..49dc1a573d1 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/listBlobs.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/listBlobs.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/listRepos.ts b/packages/pds/src/lexicon/types/com/atproto/sync/listRepos.ts index 506328d6f60..afbc9df8475 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/listRepos.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/listRepos.ts @@ -35,13 +35,16 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput export interface Repo { did: string diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/notifyOfUpdate.ts b/packages/pds/src/lexicon/types/com/atproto/sync/notifyOfUpdate.ts index df921fb8bc2..3d310c1139a 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/notifyOfUpdate.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/notifyOfUpdate.ts @@ -27,10 +27,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/requestCrawl.ts b/packages/pds/src/lexicon/types/com/atproto/sync/requestCrawl.ts index c634409f6ba..87ef20d7297 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/requestCrawl.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/requestCrawl.ts @@ -27,10 +27,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | void -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/subscribeRepos.ts b/packages/pds/src/lexicon/types/com/atproto/sync/subscribeRepos.ts index b0bf73edaca..9b5326ed347 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/subscribeRepos.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/subscribeRepos.ts @@ -22,12 +22,15 @@ export type OutputSchema = | { $type: string; [k: string]: unknown } export type HandlerError = ErrorFrame<'FutureCursor' | 'ConsumerTooSlow'> export type HandlerOutput = HandlerError | OutputSchema -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams req: IncomingMessage signal: AbortSignal -}) => AsyncIterable +} +export type Handler = ( + ctx: HandlerReqCtx, +) => AsyncIterable export interface Commit { seq: number diff --git a/packages/pds/src/logger.ts b/packages/pds/src/logger.ts index 498da256a28..7a5d77ddbca 100644 --- a/packages/pds/src/logger.ts +++ b/packages/pds/src/logger.ts @@ -5,6 +5,7 @@ import * as jwt from 'jsonwebtoken' import { parseBasicAuth } from './auth' export const dbLogger = subsystemLogger('pds:db') +export const redisLogger = subsystemLogger('pds:redis') export const seqLogger = subsystemLogger('pds:sequencer') export const mailerLogger = subsystemLogger('pds:mailer') export const labelerLogger = subsystemLogger('pds:labler') diff --git a/packages/pds/src/redis.ts b/packages/pds/src/redis.ts new file mode 100644 index 00000000000..67528a11833 --- /dev/null +++ b/packages/pds/src/redis.ts @@ -0,0 +1,25 @@ +import assert from 'assert' +import { Redis } from 'ioredis' +import { redisLogger } from './logger' + +export const getRedisClient = (host: string, password?: string): Redis => { + const redisAddr = redisAddressParts(host) + const redis = new Redis({ + ...redisAddr, + password, + }) + redis.on('error', (err) => { + redisLogger.error({ host, err }, 'redis error') + }) + return redis +} + +export const redisAddressParts = ( + addr: string, + defaultPort = 6379, +): { host: string; port: number } => { + const [host, portStr, ...others] = addr.split(':') + const port = portStr ? parseInt(portStr, 10) : defaultPort + assert(host && !isNaN(port) && !others.length, `invalid address: ${addr}`) + return { host, port } +} diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index 5d0ed301d52..c1297a50967 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -93,6 +93,7 @@ export const runTestServer = async ( didCacheStaleTTL: HOUR, jwtSecret: 'jwt-secret', availableUserDomains: ['.test'], + rateLimitsEnabled: false, appUrlPasswordReset: 'app://forgot-password', emailNoReplyAddress: 'noreply@blueskyweb.xyz', publicUrl: 'https://pds.public.url', diff --git a/packages/pds/tests/rate-limits.test.ts b/packages/pds/tests/rate-limits.test.ts new file mode 100644 index 00000000000..ab260ec7ce5 --- /dev/null +++ b/packages/pds/tests/rate-limits.test.ts @@ -0,0 +1,69 @@ +import { runTestServer, TestServerInfo } from './_util' +import { SeedClient } from './seeds/client' +import userSeed from './seeds/basic' +import { AtpAgent } from '@atproto/api' +import { randomStr } from '@atproto/crypto' + +describe('rate limits', () => { + let server: TestServerInfo + let agent: AtpAgent + let sc: SeedClient + let alice: string + let bob: string + + beforeAll(async () => { + server = await runTestServer({ + dbPostgresSchema: 'rate_limits', + redisScratchAddress: process.env.REDIS_HOST, + redisScratchPassword: process.env.REDIS_PASSWORD, + rateLimitsEnabled: true, + }) + agent = new AtpAgent({ service: server.url }) + sc = new SeedClient(agent) + await userSeed(sc) + alice = sc.dids.alice + bob = sc.dids.bob + }) + + afterAll(async () => { + await server.close() + }) + + it('rate limits by ip', async () => { + const attempt = () => + agent.api.com.atproto.server.resetPassword({ + token: randomStr(4, 'base32'), + password: 'asdf1234', + }) + for (let i = 0; i < 50; i++) { + try { + await attempt() + } catch (err) { + // do nothing + } + } + await expect(attempt).rejects.toThrow('Rate Limit Exceeded') + }) + + it('rate limits by a custom key', async () => { + const attempt = () => + agent.api.com.atproto.server.createSession({ + identifier: sc.accounts[alice].handle, + password: 'asdf1234', + }) + for (let i = 0; i < 10; i++) { + try { + await attempt() + } catch (err) { + // do nothing + } + } + await expect(attempt).rejects.toThrow('Rate Limit Exceeded') + + // does not rate limit for another key + await agent.api.com.atproto.server.createSession({ + identifier: sc.accounts[bob].handle, + password: sc.accounts[bob].password, + }) + }) +}) diff --git a/packages/xrpc-server/package.json b/packages/xrpc-server/package.json index f9c03afe347..af6e84b217a 100644 --- a/packages/xrpc-server/package.json +++ b/packages/xrpc-server/package.json @@ -31,6 +31,7 @@ "express": "^4.17.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", + "rate-limiter-flexible": "^2.4.1", "uint8arrays": "3.0.0", "ws": "^8.12.0", "zod": "^3.21.4" diff --git a/packages/xrpc-server/src/index.ts b/packages/xrpc-server/src/index.ts index a1bd22ab183..1458d2ba070 100644 --- a/packages/xrpc-server/src/index.ts +++ b/packages/xrpc-server/src/index.ts @@ -2,6 +2,7 @@ export * from './types' export * from './auth' export * from './server' export * from './stream' +export * from './rate-limiter' export type { ServerTiming } from './util' export { serverTimingHeader, ServerTimer } from './util' diff --git a/packages/xrpc-server/src/rate-limiter.ts b/packages/xrpc-server/src/rate-limiter.ts new file mode 100644 index 00000000000..e650ec599ba --- /dev/null +++ b/packages/xrpc-server/src/rate-limiter.ts @@ -0,0 +1,159 @@ +import { + RateLimiterAbstract, + RateLimiterMemory, + RateLimiterRedis, + RateLimiterRes, +} from 'rate-limiter-flexible' +import { logger } from './logger' +import { + CalcKeyFn, + CalcPointsFn, + RateLimitExceededError, + RateLimiterConsume, + RateLimiterI, + RateLimiterStatus, + XRPCReqContext, +} from './types' +import { getReqIp } from './util' + +export type RateLimiterOpts = { + keyPrefix: string + durationMs: number + points: number + bypassSecret?: string + calcKey?: CalcKeyFn + calcPoints?: CalcPointsFn + failClosed?: boolean +} + +export class RateLimiter implements RateLimiterI { + public limiter: RateLimiterAbstract + private byPassSecret?: string + private failClosed?: boolean + public calcKey: CalcKeyFn + public calcPoints: CalcPointsFn + + constructor(limiter: RateLimiterAbstract, opts: RateLimiterOpts) { + this.limiter = limiter + this.byPassSecret = opts.bypassSecret + this.calcKey = opts.calcKey ?? defaultKey + this.calcPoints = opts.calcPoints ?? defaultPoints + } + + static memory(opts: RateLimiterOpts): RateLimiter { + const limiter = new RateLimiterMemory({ + keyPrefix: opts.keyPrefix, + duration: Math.floor(opts.durationMs / 1000), + points: opts.points, + }) + return new RateLimiter(limiter, opts) + } + + static redis(storeClient: unknown, opts: RateLimiterOpts): RateLimiter { + const limiter = new RateLimiterRedis({ + storeClient, + keyPrefix: opts.keyPrefix, + duration: Math.floor(opts.durationMs / 1000), + points: opts.points, + }) + return new RateLimiter(limiter, opts) + } + + async consume( + ctx: XRPCReqContext, + opts?: { calcKey?: CalcKeyFn; calcPoints?: CalcPointsFn }, + ): Promise { + if ( + this.byPassSecret && + ctx.req.header('x-ratelimit-bypass') === this.byPassSecret + ) { + return null + } + const key = opts?.calcKey ? opts.calcKey(ctx) : this.calcKey(ctx) + const points = opts?.calcPoints + ? opts.calcPoints(ctx) + : this.calcPoints(ctx) + if (points < 1) { + return null + } + try { + const res = await this.limiter.consume(key, points) + return formatLimiterStatus(this.limiter, res) + } catch (err) { + // yes this library rejects with a res not an error + if (err instanceof RateLimiterRes) { + const status = formatLimiterStatus(this.limiter, err) + throw new RateLimitExceededError(status) + } else { + if (this.failClosed) { + throw err + } + logger.error( + { + err, + keyPrefix: this.limiter.keyPrefix, + points: this.limiter.points, + duration: this.limiter.duration, + }, + 'rate limiter failed to consume points', + ) + return null + } + } + } +} + +export const formatLimiterStatus = ( + limiter: RateLimiterAbstract, + res: RateLimiterRes, +): RateLimiterStatus => { + return { + limit: limiter.points, + duration: limiter.duration, + remainingPoints: res.remainingPoints, + msBeforeNext: res.msBeforeNext, + consumedPoints: res.consumedPoints, + isFirstInDuration: res.isFirstInDuration, + } +} + +export const consumeMany = async ( + ctx: XRPCReqContext, + fns: RateLimiterConsume[], +): Promise => { + if (fns.length === 0) return + const results = await Promise.all(fns.map((fn) => fn(ctx))) + const tightestLimit = getTightestLimit(results) + if (tightestLimit !== null) { + setResHeaders(ctx, tightestLimit) + } +} + +export const setResHeaders = ( + ctx: XRPCReqContext, + status: RateLimiterStatus, +) => { + ctx.res.setHeader('RateLimit-Limit', status.limit) + ctx.res.setHeader('RateLimit-Remaining', status.remainingPoints) + ctx.res.setHeader( + 'RateLimit-Reset', + Math.floor((Date.now() + status.msBeforeNext) / 1000), + ) + ctx.res.setHeader('RateLimit-Policy', `${status.limit};w=${status.duration}`) +} + +export const getTightestLimit = ( + resps: (RateLimiterStatus | null)[], +): RateLimiterStatus | null => { + let lowest: RateLimiterStatus | null = null + for (const resp of resps) { + if (resp === null) continue + if (lowest === null || resp.remainingPoints < lowest.remainingPoints) { + lowest = resp + } + } + return lowest +} + +const defaultKey: CalcKeyFn = (ctx: XRPCReqContext) => getReqIp(ctx.req) +const defaultPoints: CalcPointsFn = () => 1 diff --git a/packages/xrpc-server/src/server.ts b/packages/xrpc-server/src/server.ts index f8dc5c82f8c..40f4baac771 100644 --- a/packages/xrpc-server/src/server.ts +++ b/packages/xrpc-server/src/server.ts @@ -30,6 +30,10 @@ import { XRPCStreamHandler, Params, InternalServerError, + XRPCReqContext, + RateLimiterI, + RateLimiterConsume, + isShared, } from './types' import { decodeQueryParams, @@ -38,6 +42,7 @@ import { validateOutput, } from './util' import log from './logger' +import { consumeMany } from './rate-limiter' export function createServer(lexicons?: unknown[], options?: Options) { return new Server(lexicons, options) @@ -50,6 +55,9 @@ export class Server { lex = new Lexicons() options: Options middleware: Record<'json' | 'text', RequestHandler> + globalRateLimiters: RateLimiterI[] + sharedRateLimiters: Record + routeRateLimiterFns: Record constructor(lexicons?: unknown[], opts?: Options) { if (lexicons) { @@ -66,6 +74,27 @@ export class Server { json: express.json({ limit: opts?.payload?.jsonLimit }), text: express.text({ limit: opts?.payload?.textLimit }), } + this.globalRateLimiters = [] + this.sharedRateLimiters = {} + this.routeRateLimiterFns = {} + if (opts?.rateLimits?.global) { + for (const limit of opts.rateLimits.global) { + const rateLimiter = opts.rateLimits.creator({ + ...limit, + keyPrefix: `rl-${limit.name}`, + }) + this.globalRateLimiters.push(rateLimiter) + } + } + if (opts?.rateLimits?.shared) { + for (const limit of opts.rateLimits.shared) { + const rateLimiter = opts.rateLimits.creator({ + ...limit, + keyPrefix: `rl-${limit.name}`, + }) + this.sharedRateLimiters[limit.name] = rateLimiter + } + } } // handlers @@ -138,6 +167,7 @@ export class Server { middleware.push(this.middleware.json) middleware.push(this.middleware.text) } + this.setupRouteRateLimits(nsid, config) this.routes[verb]( `/xrpc/${nsid}`, ...middleware, @@ -185,6 +215,10 @@ export class Server { validateOutput(nsid, def, output, this.lex) const assertValidXrpcParams = (params: unknown) => this.lex.assertValidXrpcParams(nsid, params) + const rlFns = this.routeRateLimiterFns[nsid] ?? [] + const consumeRateLimit = (reqCtx: XRPCReqContext) => + consumeMany(reqCtx, rlFns) + return async function (req, res, next) { try { // validate request @@ -203,14 +237,21 @@ export class Server { const locals: RequestLocals = req[kRequestLocals] - // run the handler - const outputUnvalidated = await handler({ + const reqCtx: XRPCReqContext = { params, input, auth: locals.auth, req, res, - }) + } + + // handle rate limits + if (consumeRateLimit) { + await consumeRateLimit(reqCtx) + } + + // run the handler + const outputUnvalidated = await handler(reqCtx) if (isHandlerError(outputUnvalidated)) { throw XRPCError.fromError(outputUnvalidated) @@ -345,6 +386,55 @@ export class Server { return httpServer } } + + private setupRouteRateLimits(nsid: string, config: XRPCHandlerConfig) { + this.routeRateLimiterFns[nsid] = [] + for (const limit of this.globalRateLimiters) { + const consumeFn = async (ctx: XRPCReqContext) => { + return limit.consume(ctx) + } + this.routeRateLimiterFns[nsid].push(consumeFn) + } + + if (config.rateLimit) { + const limits = Array.isArray(config.rateLimit) + ? config.rateLimit + : [config.rateLimit] + this.routeRateLimiterFns[nsid] = [] + for (const limit of limits) { + const { calcKey, calcPoints } = limit + if (isShared(limit)) { + const rateLimiter = this.sharedRateLimiters[limit.name] + if (rateLimiter) { + const consumeFn = (ctx: XRPCReqContext) => + rateLimiter.consume(ctx, { + calcKey, + calcPoints, + }) + this.routeRateLimiterFns[nsid].push(consumeFn) + } + } else { + const { durationMs, points } = limit + const rateLimiter = this.options.rateLimits?.creator({ + keyPrefix: nsid, + durationMs, + points, + calcKey, + calcPoints, + }) + if (rateLimiter) { + this.sharedRateLimiters[nsid] = rateLimiter + const consumeFn = (ctx: XRPCReqContext) => + rateLimiter.consume(ctx, { + calcKey, + calcPoints, + }) + this.routeRateLimiterFns[nsid].push(consumeFn) + } + } + } + } + } } function isHandlerSuccess(v: HandlerOutput): v is HandlerSuccess { diff --git a/packages/xrpc-server/src/types.ts b/packages/xrpc-server/src/types.ts index 266a8db222a..801c8baa6f2 100644 --- a/packages/xrpc-server/src/types.ts +++ b/packages/xrpc-server/src/types.ts @@ -15,6 +15,11 @@ export type Options = { blobLimit?: number textLimit?: number } + rateLimits?: { + creator: RateLimiterCreator + global?: ServerRateLimitDescription[] + shared?: ServerRateLimitDescription[] + } } export type UndecodedParams = typeof express.request['query'] @@ -50,13 +55,17 @@ export type HandlerError = zod.infer export type HandlerOutput = HandlerSuccess | HandlerError -export type XRPCHandler = (ctx: { +export type XRPCReqContext = { auth: HandlerAuth | undefined params: Params input: HandlerInput | undefined req: express.Request res: express.Response -}) => Promise | HandlerOutput | undefined +} + +export type XRPCHandler = ( + ctx: XRPCReqContext, +) => Promise | HandlerOutput | undefined export type XRPCStreamHandler = (ctx: { auth: HandlerAuth | undefined @@ -76,7 +85,66 @@ export type StreamAuthVerifier = (ctx: { req: IncomingMessage }) => Promise | AuthOutput +export type CalcKeyFn = (ctx: XRPCReqContext) => string +export type CalcPointsFn = (ctx: XRPCReqContext) => number + +export interface RateLimiterI { + consume: RateLimiterConsume +} + +export type RateLimiterConsume = ( + ctx: XRPCReqContext, + opts?: { calcKey?: CalcKeyFn; calcPoints?: CalcPointsFn }, +) => Promise + +export type RateLimiterCreator = (opts: { + keyPrefix: string + durationMs: number + points: number + calcKey?: (ctx: XRPCReqContext) => string + calcPoints?: (ctx: XRPCReqContext) => number +}) => RateLimiterI + +export type ServerRateLimitDescription = { + name: string + durationMs: number + points: number + calcKey?: (ctx: XRPCReqContext) => string + calcPoints?: (ctx: XRPCReqContext) => number +} + +export type SharedRateLimitOpts = { + name: string + calcKey?: (ctx: XRPCReqContext) => string + calcPoints?: (ctx: XRPCReqContext) => number +} + +export type RouteRateLimitOpts = { + durationMs: number + points: number + calcKey?: (ctx: XRPCReqContext) => string + calcPoints?: (ctx: XRPCReqContext) => number +} + +export type HandlerRateLimitOpts = SharedRateLimitOpts | RouteRateLimitOpts + +export const isShared = ( + opts: HandlerRateLimitOpts, +): opts is SharedRateLimitOpts => { + return typeof opts['name'] === 'string' +} + +export type RateLimiterStatus = { + limit: number + duration: number + remainingPoints: number + msBeforeNext: number + consumedPoints: number + isFirstInDuration: boolean +} + export type XRPCHandlerConfig = { + rateLimit?: HandlerRateLimitOpts | HandlerRateLimitOpts[] auth?: AuthVerifier handler: XRPCHandler } @@ -154,6 +222,16 @@ export class ForbiddenError extends XRPCError { } } +export class RateLimitExceededError extends XRPCError { + constructor( + status: RateLimiterStatus, + errorMessage?: string, + customErrorName?: string, + ) { + super(ResponseType.RateLimitExceeded, errorMessage, customErrorName) + } +} + export class InternalServerError extends XRPCError { constructor(errorMessage?: string, customErrorName?: string) { super(ResponseType.InternalServerError, errorMessage, customErrorName) diff --git a/packages/xrpc-server/src/util.ts b/packages/xrpc-server/src/util.ts index 730db950fbd..813587382ba 100644 --- a/packages/xrpc-server/src/util.ts +++ b/packages/xrpc-server/src/util.ts @@ -268,6 +268,10 @@ function decodeBodyStream( return stream } +export const getReqIp = (req: express.Request): string => { + return req.ips.at(-1) ?? req.ip +} + export function serverTimingHeader(timings: ServerTiming[]) { return timings .map((timing) => { diff --git a/packages/xrpc-server/tests/rate-limiter.test.ts b/packages/xrpc-server/tests/rate-limiter.test.ts new file mode 100644 index 00000000000..09a235a38b9 --- /dev/null +++ b/packages/xrpc-server/tests/rate-limiter.test.ts @@ -0,0 +1,249 @@ +import * as http from 'http' +import getPort from 'get-port' +import xrpc, { ServiceClient } from '@atproto/xrpc' +import { createServer, closeServer } from './_util' +import * as xrpcServer from '../src' +import { RateLimiter } from '../src' +import { MINUTE } from '@atproto/common' + +const LEXICONS = [ + { + lexicon: 1, + id: 'io.example.routeLimit', + defs: { + main: { + type: 'query', + parameters: { + type: 'params', + required: ['str'], + properties: { + str: { type: 'string' }, + }, + }, + output: { + encoding: 'application/json', + }, + }, + }, + }, + { + lexicon: 1, + id: 'io.example.sharedLimitOne', + defs: { + main: { + type: 'query', + parameters: { + type: 'params', + required: ['points'], + properties: { + points: { type: 'integer' }, + }, + }, + output: { + encoding: 'application/json', + }, + }, + }, + }, + { + lexicon: 1, + id: 'io.example.sharedLimitTwo', + defs: { + main: { + type: 'query', + parameters: { + type: 'params', + required: ['points'], + properties: { + points: { type: 'integer' }, + }, + }, + output: { + encoding: 'application/json', + }, + }, + }, + }, + { + lexicon: 1, + id: 'io.example.toggleLimit', + defs: { + main: { + type: 'query', + parameters: { + type: 'params', + properties: { + shouldCount: { type: 'boolean' }, + }, + }, + output: { + encoding: 'application/json', + }, + }, + }, + }, + { + lexicon: 1, + id: 'io.example.noLimit', + defs: { + main: { + type: 'query', + output: { + encoding: 'application/json', + }, + }, + }, + }, +] + +describe('Parameters', () => { + let s: http.Server + const server = xrpcServer.createServer(LEXICONS, { + rateLimits: { + creator: (opts: xrpcServer.RateLimiterOpts) => + RateLimiter.memory({ + bypassSecret: 'bypass', + ...opts, + }), + shared: [ + { + name: 'shared-limit', + durationMs: 5 * MINUTE, + points: 6, + }, + ], + global: [ + { + name: 'global-ip', + durationMs: 5 * MINUTE, + points: 100, + }, + ], + }, + }) + server.method('io.example.routeLimit', { + rateLimit: { + durationMs: 5 * MINUTE, + points: 5, + calcKey: ({ params }) => params.str as string, + }, + handler: (ctx: { params: xrpcServer.Params }) => ({ + encoding: 'json', + body: ctx.params, + }), + }) + + server.method('io.example.sharedLimitOne', { + rateLimit: { + name: 'shared-limit', + calcPoints: ({ params }) => params.points as number, + }, + handler: (ctx: { params: xrpcServer.Params }) => ({ + encoding: 'json', + body: ctx.params, + }), + }) + server.method('io.example.sharedLimitTwo', { + rateLimit: { + name: 'shared-limit', + calcPoints: ({ params }) => params.points as number, + }, + handler: (ctx: { params: xrpcServer.Params }) => ({ + encoding: 'json', + body: ctx.params, + }), + }) + server.method('io.example.toggleLimit', { + rateLimit: [ + { + durationMs: 5 * MINUTE, + points: 5, + calcPoints: ({ params }) => (params.shouldCount ? 1 : 0), + }, + { + durationMs: 5 * MINUTE, + points: 10, + }, + ], + handler: (ctx: { params: xrpcServer.Params }) => ({ + encoding: 'json', + body: ctx.params, + }), + }) + server.method('io.example.noLimit', { + handler: () => ({ + encoding: 'json', + body: {}, + }), + }) + + xrpc.addLexicons(LEXICONS) + + let client: ServiceClient + beforeAll(async () => { + const port = await getPort() + s = await createServer(port, server) + client = xrpc.service(`http://localhost:${port}`) + }) + afterAll(async () => { + await closeServer(s) + }) + + it('rate limits a given route', async () => { + const makeCall = () => client.call('io.example.routeLimit', { str: 'test' }) + for (let i = 0; i < 5; i++) { + await makeCall() + } + await expect(makeCall).rejects.toThrow('Rate Limit Exceeded') + }) + + it('rate limits on a shared route', async () => { + await client.call('io.example.sharedLimitOne', { points: 1 }) + await client.call('io.example.sharedLimitTwo', { points: 1 }) + await client.call('io.example.sharedLimitOne', { points: 2 }) + await client.call('io.example.sharedLimitTwo', { points: 2 }) + await expect( + client.call('io.example.sharedLimitOne', { points: 1 }), + ).rejects.toThrow('Rate Limit Exceeded') + await expect( + client.call('io.example.sharedLimitTwo', { points: 1 }), + ).rejects.toThrow('Rate Limit Exceeded') + }) + + it('applies multiple rate-limits', async () => { + const makeCall = (shouldCount: boolean) => + client.call('io.example.toggleLimit', { shouldCount }) + for (let i = 0; i < 5; i++) { + await makeCall(true) + } + await expect(() => makeCall(true)).rejects.toThrow('Rate Limit Exceeded') + for (let i = 0; i < 4; i++) { + await makeCall(false) + } + await expect(() => makeCall(false)).rejects.toThrow('Rate Limit Exceeded') + }) + + it('applies global limits', async () => { + const makeCall = () => client.call('io.example.noLimit') + const calls: Promise[] = [] + for (let i = 0; i < 110; i++) { + calls.push(makeCall()) + } + await expect(Promise.all(calls)).rejects.toThrow('Rate Limit Exceeded') + }) + + it('can bypass rate limits', async () => { + const makeCall = () => + client.call( + 'io.example.noLimit', + {}, + {}, + { headers: { 'X-RateLimit-Bypass': 'bypass' } }, + ) + const calls: Promise[] = [] + for (let i = 0; i < 110; i++) { + calls.push(makeCall()) + } + await Promise.all(calls) + }) +}) diff --git a/yarn.lock b/yarn.lock index 0eec71190f5..ee4cebbfc37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10433,6 +10433,11 @@ range-parser@~1.2.1: resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== +rate-limiter-flexible@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/rate-limiter-flexible/-/rate-limiter-flexible-2.4.1.tgz#c74cfe36ac2cbfe56f68ded9a3b4b2fde1963c41" + integrity sha512-dgH4T44TzKVO9CLArNto62hJOwlWJMLUjVVr/ii0uUzZXEXthDNr7/yefW5z/1vvHAfycc1tnuiYyNJ8CTRB3g== + raw-body@2.5.1: version "2.5.1" resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz" From a8b3f62fab0253eddf1d56fc5bd739b16cfe6f0b Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 21 Aug 2023 16:35:19 -0500 Subject: [PATCH 173/237] Bugfix: Application ratelimits relative import (#1500) dont use relative xrpc-server path --- packages/pds/src/api/com/atproto/server/createSession.ts | 3 +-- packages/xrpc-server/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/pds/src/api/com/atproto/server/createSession.ts b/packages/pds/src/api/com/atproto/server/createSession.ts index bd62a590638..01274ff4618 100644 --- a/packages/pds/src/api/com/atproto/server/createSession.ts +++ b/packages/pds/src/api/com/atproto/server/createSession.ts @@ -1,10 +1,9 @@ -import { AuthRequiredError } from '@atproto/xrpc-server' +import { AuthRequiredError, getReqIp } from '@atproto/xrpc-server' import AppContext from '../../../../context' import { softDeleted } from '../../../../db/util' import { Server } from '../../../../lexicon' import { AuthScope } from '../../../../auth' import { DAY, MINUTE } from '@atproto/common' -import { getReqIp } from '@atproto/xrpc-server/src/util' export default function (server: Server, ctx: AppContext) { server.com.atproto.server.createSession({ diff --git a/packages/xrpc-server/src/index.ts b/packages/xrpc-server/src/index.ts index 1458d2ba070..6bc4147cb44 100644 --- a/packages/xrpc-server/src/index.ts +++ b/packages/xrpc-server/src/index.ts @@ -5,4 +5,4 @@ export * from './stream' export * from './rate-limiter' export type { ServerTiming } from './util' -export { serverTimingHeader, ServerTimer } from './util' +export { getReqIp, serverTimingHeader, ServerTimer } from './util' From e938228db4abf3a467fb1519b7064582c23e0fa6 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Mon, 21 Aug 2023 19:06:24 -0400 Subject: [PATCH 174/237] Fix auth on bsky author feed (#1501) fix auth on bsky author feed --- packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index b5dac940cf1..06b93513444 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -7,10 +7,11 @@ import { setRepoRev } from '../../../util' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getAuthorFeed({ - auth: ctx.authOptionalVerifier, + auth: ctx.authOptionalAccessOrRoleVerifier, handler: async ({ params, auth, res }) => { const { actor, limit, cursor, filter } = params - const viewer = auth.credentials.did + const viewer = + auth.credentials.type === 'access' ? auth.credentials.did : null const db = ctx.db.getReplica() const { ref } = db.db.dynamic From ad25430691a3e90fd88c6f2954340b9e623fd400 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 21 Aug 2023 18:13:01 -0500 Subject: [PATCH 175/237] Tweak rate limits (#1502) * tweak rate limits * tweak # * fix test --- .../pds/src/api/com/atproto/server/createSession.ts | 12 +++++------- packages/pds/tests/rate-limits.test.ts | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/pds/src/api/com/atproto/server/createSession.ts b/packages/pds/src/api/com/atproto/server/createSession.ts index 01274ff4618..aee0063d86c 100644 --- a/packages/pds/src/api/com/atproto/server/createSession.ts +++ b/packages/pds/src/api/com/atproto/server/createSession.ts @@ -1,4 +1,4 @@ -import { AuthRequiredError, getReqIp } from '@atproto/xrpc-server' +import { AuthRequiredError } from '@atproto/xrpc-server' import AppContext from '../../../../context' import { softDeleted } from '../../../../db/util' import { Server } from '../../../../lexicon' @@ -10,15 +10,13 @@ export default function (server: Server, ctx: AppContext) { rateLimit: [ { durationMs: DAY, - points: 200, - calcKey: ({ req, input }) => - `${getReqIp(req)}-${input.body.identifier}`, + points: 300, + calcKey: ({ input }) => input.body.identifier, }, { durationMs: 5 * MINUTE, - points: 10, - calcKey: ({ req, input }) => - `${getReqIp(req)}-${input.body.identifier}`, + points: 30, + calcKey: ({ input }) => input.body.identifier, }, ], handler: async ({ input }) => { diff --git a/packages/pds/tests/rate-limits.test.ts b/packages/pds/tests/rate-limits.test.ts index ab260ec7ce5..6f7cd77cbb8 100644 --- a/packages/pds/tests/rate-limits.test.ts +++ b/packages/pds/tests/rate-limits.test.ts @@ -51,7 +51,7 @@ describe('rate limits', () => { identifier: sc.accounts[alice].handle, password: 'asdf1234', }) - for (let i = 0; i < 10; i++) { + for (let i = 0; i < 30; i++) { try { await attempt() } catch (err) { From 4cdb43c7f3ae5224936ad9f68523e9d9dd9a013e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 22 Aug 2023 11:07:06 -0500 Subject: [PATCH 176/237] view likes from muted accounts --- .../src/api/app/bsky/feed/getActorLikes.ts | 5 ---- packages/bsky/tests/views/actor-likes.test.ts | 25 ------------------- .../api/app/bsky/feed/getActorLikes.ts | 9 +------ packages/pds/tests/views/actor-likes.test.ts | 23 ----------------- 4 files changed, 1 insertion(+), 61 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts index 3a757ed31cb..00e363b28f3 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts @@ -36,11 +36,6 @@ export default function (server: Server, ctx: AppContext) { if (viewer !== null) { feedItemsQb = feedItemsQb - .where((qb) => - qb.where((qb) => - graphService.whereNotMuted(qb, viewer, [ref('post.creator')]), - ), - ) .whereNotExists(graphService.blockQb(viewer, [ref('post.creator')])) } diff --git a/packages/bsky/tests/views/actor-likes.test.ts b/packages/bsky/tests/views/actor-likes.test.ts index 8dab3aa7d62..774c3e63141 100644 --- a/packages/bsky/tests/views/actor-likes.test.ts +++ b/packages/bsky/tests/views/actor-likes.test.ts @@ -123,29 +123,4 @@ describe('bsky actor likes feed views', () => { sc.getHeaders(alice), ) }) - - it('viewer has muted author of liked post(s)', async () => { - await pdsAgent.api.app.bsky.graph.muteActor( - { actor: alice }, // bob mutes alice - { headers: sc.getHeaders(bob), encoding: 'application/json' }, - ) - - await network.processAll() - - const { data } = await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[bob].handle }, // bob has liked alice's posts - { headers: await network.serviceHeaders(bob) }, - ) - - expect( - data.feed.every((item) => { - return item.post.author.did !== alice - }), - ).toBe(true) // alice's posts are filtered out - - await pdsAgent.api.app.bsky.graph.unmuteActor( - { actor: alice }, // dan unmutes alice - { headers: sc.getHeaders(bob), encoding: 'application/json' }, - ) - }) }) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts index 4fdaffc768e..ec5d3e8f001 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts @@ -57,16 +57,9 @@ export default function (server: Server, ctx: AppContext) { .innerJoin('like', 'like.subject', 'feed_item.uri') .where('like.creator', '=', actorDid) - // for access-based auth, enforce blocks and mutes + // for access-based auth, enforce blocks if (requester) { feedItemsQb = feedItemsQb - .where((qb) => - qb.where((qb) => - accountService.whereNotMuted(qb, requester, [ - ref('post.creator'), - ]), - ), - ) .whereNotExists( graphService.blockQb(requester, [ref('post.creator')]), ) diff --git a/packages/pds/tests/views/actor-likes.test.ts b/packages/pds/tests/views/actor-likes.test.ts index 966aabf6197..27c1ab4385c 100644 --- a/packages/pds/tests/views/actor-likes.test.ts +++ b/packages/pds/tests/views/actor-likes.test.ts @@ -114,27 +114,4 @@ describe('pds actor likes feed views', () => { sc.getHeaders(alice), ) }) - - it('viewer has muted author of liked post(s)', async () => { - await agent.api.app.bsky.graph.muteActor( - { actor: alice }, // bob mutes alice - { headers: sc.getHeaders(bob), encoding: 'application/json' }, - ) - - const { data } = await agent.api.app.bsky.feed.getActorLikes( - { actor: sc.accounts[bob].handle }, // bob has liked alice's posts - { headers: sc.getHeaders(bob) }, - ) - - expect( - data.feed.every((item) => { - return item.post.author.did !== alice - }), - ).toBe(true) // alice's posts are filtered out - - await agent.api.app.bsky.graph.unmuteActor( - { actor: alice }, // dan unmutes alice - { headers: sc.getHeaders(bob), encoding: 'application/json' }, - ) - }) }) From e5c96f596acf58cd8b71fdbb304b83f5615aee27 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 22 Aug 2023 11:15:09 -0500 Subject: [PATCH 177/237] format --- packages/bsky/src/api/app/bsky/feed/getActorLikes.ts | 5 +++-- .../pds/src/app-view/api/app/bsky/feed/getActorLikes.ts | 7 +++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts index 00e363b28f3..7c634e0a810 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts @@ -35,8 +35,9 @@ export default function (server: Server, ctx: AppContext) { .where('like.creator', '=', actorDid) if (viewer !== null) { - feedItemsQb = feedItemsQb - .whereNotExists(graphService.blockQb(viewer, [ref('post.creator')])) + feedItemsQb = feedItemsQb.whereNotExists( + graphService.blockQb(viewer, [ref('post.creator')]), + ) } const keyset = new FeedKeyset( diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts index ec5d3e8f001..2e337e4f629 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts @@ -59,10 +59,9 @@ export default function (server: Server, ctx: AppContext) { // for access-based auth, enforce blocks if (requester) { - feedItemsQb = feedItemsQb - .whereNotExists( - graphService.blockQb(requester, [ref('post.creator')]), - ) + feedItemsQb = feedItemsQb.whereNotExists( + graphService.blockQb(requester, [ref('post.creator')]), + ) } const keyset = new FeedKeyset( From 98cc8c52142caf9351a87c270a27074f149d4f45 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 22 Aug 2023 11:16:56 -0500 Subject: [PATCH 178/237] codegen --- packages/api/src/client/index.ts | 26 +++++++-------- packages/api/src/client/lexicons.ts | 32 +++++++++---------- packages/bsky/src/lexicon/index.ts | 20 +++++++----- packages/bsky/src/lexicon/lexicons.ts | 32 +++++++++---------- .../types/app/bsky/feed/getActorLikes.ts | 7 ++-- packages/pds/src/lexicon/index.ts | 20 +++++++----- packages/pds/src/lexicon/lexicons.ts | 32 +++++++++---------- .../types/app/bsky/feed/getActorLikes.ts | 7 ++-- 8 files changed, 95 insertions(+), 81 deletions(-) diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index e4971107dac..4a7b9081321 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -89,8 +89,8 @@ import * as AppBskyFeedDefs from './types/app/bsky/feed/defs' import * as AppBskyFeedDescribeFeedGenerator from './types/app/bsky/feed/describeFeedGenerator' import * as AppBskyFeedGenerator from './types/app/bsky/feed/generator' import * as AppBskyFeedGetActorFeeds from './types/app/bsky/feed/getActorFeeds' -import * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed' 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' @@ -210,8 +210,8 @@ export * as AppBskyFeedDefs from './types/app/bsky/feed/defs' export * as AppBskyFeedDescribeFeedGenerator from './types/app/bsky/feed/describeFeedGenerator' export * as AppBskyFeedGenerator from './types/app/bsky/feed/generator' export * as AppBskyFeedGetActorFeeds from './types/app/bsky/feed/getActorFeeds' -export * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed' export * as AppBskyFeedGetActorLikes from './types/app/bsky/feed/getActorLikes' +export * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed' export * as AppBskyFeedGetFeed from './types/app/bsky/feed/getFeed' export * as AppBskyFeedGetFeedGenerator from './types/app/bsky/feed/getFeedGenerator' export * as AppBskyFeedGetFeedGenerators from './types/app/bsky/feed/getFeedGenerators' @@ -1248,17 +1248,6 @@ export class FeedNS { }) } - getAuthorFeed( - params?: AppBskyFeedGetAuthorFeed.QueryParams, - opts?: AppBskyFeedGetAuthorFeed.CallOptions, - ): Promise { - return this._service.xrpc - .call('app.bsky.feed.getAuthorFeed', params, undefined, opts) - .catch((e) => { - throw AppBskyFeedGetAuthorFeed.toKnownErr(e) - }) - } - getActorLikes( params?: AppBskyFeedGetActorLikes.QueryParams, opts?: AppBskyFeedGetActorLikes.CallOptions, @@ -1270,6 +1259,17 @@ export class FeedNS { }) } + getAuthorFeed( + params?: AppBskyFeedGetAuthorFeed.QueryParams, + opts?: AppBskyFeedGetAuthorFeed.CallOptions, + ): Promise { + return this._service.xrpc + .call('app.bsky.feed.getAuthorFeed', params, undefined, opts) + .catch((e) => { + throw AppBskyFeedGetAuthorFeed.toKnownErr(e) + }) + } + getFeed( params?: AppBskyFeedGetFeed.QueryParams, opts?: AppBskyFeedGetFeed.CallOptions, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 64a6e5f8136..ab7e2febe1b 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -4816,13 +4816,13 @@ export const schemaDict = { }, }, }, - AppBskyFeedGetAuthorFeed: { + AppBskyFeedGetActorLikes: { lexicon: 1, - id: 'app.bsky.feed.getAuthorFeed', + id: 'app.bsky.feed.getActorLikes', defs: { main: { type: 'query', - description: "A view of an actor's feed.", + description: 'A view of the posts liked by an actor.', parameters: { type: 'params', required: ['actor'], @@ -4840,15 +4840,6 @@ export const schemaDict = { cursor: { type: 'string', }, - filter: { - type: 'string', - knownValues: [ - 'posts_with_replies', - 'posts_no_replies', - 'posts_with_media', - ], - default: 'posts_with_replies', - }, }, }, output: { @@ -4881,13 +4872,13 @@ export const schemaDict = { }, }, }, - AppBskyFeedGetActorLikes: { + AppBskyFeedGetAuthorFeed: { lexicon: 1, - id: 'app.bsky.feed.getActorLikes', + id: 'app.bsky.feed.getAuthorFeed', defs: { main: { type: 'query', - description: 'A view of the posts liked by an actor.', + description: "A view of an actor's feed.", parameters: { type: 'params', required: ['actor'], @@ -4905,6 +4896,15 @@ export const schemaDict = { cursor: { type: 'string', }, + filter: { + type: 'string', + knownValues: [ + 'posts_with_replies', + 'posts_no_replies', + 'posts_with_media', + ], + default: 'posts_with_replies', + }, }, }, output: { @@ -6713,8 +6713,8 @@ export const ids = { AppBskyFeedDescribeFeedGenerator: 'app.bsky.feed.describeFeedGenerator', AppBskyFeedGenerator: 'app.bsky.feed.generator', AppBskyFeedGetActorFeeds: 'app.bsky.feed.getActorFeeds', - AppBskyFeedGetAuthorFeed: 'app.bsky.feed.getAuthorFeed', 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', diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index c91cac40dec..8b33f2c2c21 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -78,8 +78,8 @@ 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 AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed' 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' @@ -1044,21 +1044,25 @@ export class FeedNS { return this._server.xrpc.method(nsid, cfg) } - getAuthorFeed( + getActorLikes( cfg: ConfigOf< AV, - AppBskyFeedGetAuthorFeed.Handler>, - AppBskyFeedGetAuthorFeed.HandlerReqCtx> + AppBskyFeedGetActorLikes.Handler>, + AppBskyFeedGetActorLikes.HandlerReqCtx> >, ) { - const nsid = 'app.bsky.feed.getAuthorFeed' // @ts-ignore + const nsid = 'app.bsky.feed.getActorLikes' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } - getActorLikes( - cfg: ConfigOf>>, + getAuthorFeed( + cfg: ConfigOf< + AV, + AppBskyFeedGetAuthorFeed.Handler>, + AppBskyFeedGetAuthorFeed.HandlerReqCtx> + >, ) { - const nsid = 'app.bsky.feed.getActorLikes' // @ts-ignore + const nsid = 'app.bsky.feed.getAuthorFeed' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 64a6e5f8136..ab7e2febe1b 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -4816,13 +4816,13 @@ export const schemaDict = { }, }, }, - AppBskyFeedGetAuthorFeed: { + AppBskyFeedGetActorLikes: { lexicon: 1, - id: 'app.bsky.feed.getAuthorFeed', + id: 'app.bsky.feed.getActorLikes', defs: { main: { type: 'query', - description: "A view of an actor's feed.", + description: 'A view of the posts liked by an actor.', parameters: { type: 'params', required: ['actor'], @@ -4840,15 +4840,6 @@ export const schemaDict = { cursor: { type: 'string', }, - filter: { - type: 'string', - knownValues: [ - 'posts_with_replies', - 'posts_no_replies', - 'posts_with_media', - ], - default: 'posts_with_replies', - }, }, }, output: { @@ -4881,13 +4872,13 @@ export const schemaDict = { }, }, }, - AppBskyFeedGetActorLikes: { + AppBskyFeedGetAuthorFeed: { lexicon: 1, - id: 'app.bsky.feed.getActorLikes', + id: 'app.bsky.feed.getAuthorFeed', defs: { main: { type: 'query', - description: 'A view of the posts liked by an actor.', + description: "A view of an actor's feed.", parameters: { type: 'params', required: ['actor'], @@ -4905,6 +4896,15 @@ export const schemaDict = { cursor: { type: 'string', }, + filter: { + type: 'string', + knownValues: [ + 'posts_with_replies', + 'posts_no_replies', + 'posts_with_media', + ], + default: 'posts_with_replies', + }, }, }, output: { @@ -6713,8 +6713,8 @@ export const ids = { AppBskyFeedDescribeFeedGenerator: 'app.bsky.feed.describeFeedGenerator', AppBskyFeedGenerator: 'app.bsky.feed.generator', AppBskyFeedGetActorFeeds: 'app.bsky.feed.getActorFeeds', - AppBskyFeedGetAuthorFeed: 'app.bsky.feed.getAuthorFeed', 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', diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getActorLikes.ts index ce56c2667fd..df2f291e1a7 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/feed/getActorLikes.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/feed/getActorLikes.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index c91cac40dec..8b33f2c2c21 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -78,8 +78,8 @@ 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 AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed' 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' @@ -1044,21 +1044,25 @@ export class FeedNS { return this._server.xrpc.method(nsid, cfg) } - getAuthorFeed( + getActorLikes( cfg: ConfigOf< AV, - AppBskyFeedGetAuthorFeed.Handler>, - AppBskyFeedGetAuthorFeed.HandlerReqCtx> + AppBskyFeedGetActorLikes.Handler>, + AppBskyFeedGetActorLikes.HandlerReqCtx> >, ) { - const nsid = 'app.bsky.feed.getAuthorFeed' // @ts-ignore + const nsid = 'app.bsky.feed.getActorLikes' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } - getActorLikes( - cfg: ConfigOf>>, + getAuthorFeed( + cfg: ConfigOf< + AV, + AppBskyFeedGetAuthorFeed.Handler>, + AppBskyFeedGetAuthorFeed.HandlerReqCtx> + >, ) { - const nsid = 'app.bsky.feed.getActorLikes' // @ts-ignore + const nsid = 'app.bsky.feed.getAuthorFeed' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 64a6e5f8136..ab7e2febe1b 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -4816,13 +4816,13 @@ export const schemaDict = { }, }, }, - AppBskyFeedGetAuthorFeed: { + AppBskyFeedGetActorLikes: { lexicon: 1, - id: 'app.bsky.feed.getAuthorFeed', + id: 'app.bsky.feed.getActorLikes', defs: { main: { type: 'query', - description: "A view of an actor's feed.", + description: 'A view of the posts liked by an actor.', parameters: { type: 'params', required: ['actor'], @@ -4840,15 +4840,6 @@ export const schemaDict = { cursor: { type: 'string', }, - filter: { - type: 'string', - knownValues: [ - 'posts_with_replies', - 'posts_no_replies', - 'posts_with_media', - ], - default: 'posts_with_replies', - }, }, }, output: { @@ -4881,13 +4872,13 @@ export const schemaDict = { }, }, }, - AppBskyFeedGetActorLikes: { + AppBskyFeedGetAuthorFeed: { lexicon: 1, - id: 'app.bsky.feed.getActorLikes', + id: 'app.bsky.feed.getAuthorFeed', defs: { main: { type: 'query', - description: 'A view of the posts liked by an actor.', + description: "A view of an actor's feed.", parameters: { type: 'params', required: ['actor'], @@ -4905,6 +4896,15 @@ export const schemaDict = { cursor: { type: 'string', }, + filter: { + type: 'string', + knownValues: [ + 'posts_with_replies', + 'posts_no_replies', + 'posts_with_media', + ], + default: 'posts_with_replies', + }, }, }, output: { @@ -6713,8 +6713,8 @@ export const ids = { AppBskyFeedDescribeFeedGenerator: 'app.bsky.feed.describeFeedGenerator', AppBskyFeedGenerator: 'app.bsky.feed.generator', AppBskyFeedGetActorFeeds: 'app.bsky.feed.getActorFeeds', - AppBskyFeedGetAuthorFeed: 'app.bsky.feed.getAuthorFeed', 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', diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getActorLikes.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getActorLikes.ts index ce56c2667fd..df2f291e1a7 100644 --- a/packages/pds/src/lexicon/types/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/lexicon/types/app/bsky/feed/getActorLikes.ts @@ -38,10 +38,13 @@ export interface HandlerError { } export type HandlerOutput = HandlerError | HandlerSuccess -export type Handler = (ctx: { +export type HandlerReqCtx = { auth: HA params: QueryParams input: HandlerInput req: express.Request res: express.Response -}) => Promise | HandlerOutput +} +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput From c28013b6851aeb453ca524f457f9952073386a93 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 22 Aug 2023 11:21:26 -0500 Subject: [PATCH 179/237] remove unused --- packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts index 2e337e4f629..74f1581db9a 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getActorLikes.ts @@ -35,7 +35,6 @@ export default function (server: Server, ctx: AppContext) { const { actor, limit, cursor } = params const { ref } = ctx.db.db.dynamic - const accountService = ctx.services.account(ctx.db) const actorService = ctx.services.appView.actor(ctx.db) const feedService = ctx.services.appView.feed(ctx.db) const graphService = ctx.services.appView.graph(ctx.db) @@ -119,7 +118,6 @@ const getAuthorMunge = async ( } }) } - feed = await localSrvc.formatAndInsertPostsInFeed(feed, local.posts) return { ...original, feed, From ee0c9c7e8c5042eac8f9c62b253e40fd40dcc648 Mon Sep 17 00:00:00 2001 From: Ryan Barrett Date: Tue, 22 Aug 2023 09:46:35 -0700 Subject: [PATCH 180/237] com.atproto.sync.subscribeRepos lexicon: add #repoOp description (#1503) specifically, note that it's null for deletes. for https://github.com/bluesky-social/atproto/discussions/1252 , cc @ DavidBuchanan314 --- lexicons/com/atproto/sync/subscribeRepos.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lexicons/com/atproto/sync/subscribeRepos.json b/lexicons/com/atproto/sync/subscribeRepos.json index 04a633447ca..df26c131ccd 100644 --- a/lexicons/com/atproto/sync/subscribeRepos.json +++ b/lexicons/com/atproto/sync/subscribeRepos.json @@ -104,6 +104,7 @@ }, "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": { @@ -120,4 +121,4 @@ } } } -} \ No newline at end of file +} From 7e4edb2ce1006fef7fe7247430532b8ae53303ed Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 22 Aug 2023 12:13:26 -0700 Subject: [PATCH 181/237] @atproto/api@0.6.5 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 16d3c5a9aa5..cfb25c636f9 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.4", + "version": "0.6.5", "main": "src/index.ts", "scripts": { "codegen": "yarn docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From 831fe9fc29aecd4378da67463411ccf215d85608 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Tue, 22 Aug 2023 21:07:07 -0400 Subject: [PATCH 182/237] Handle negative ISO year for indexing (#1505) * handle negative iso year * fix pkg main * tidy --- packages/bsky/package.json | 1 + packages/bsky/src/services/indexing/util.ts | 9 ++++++++- packages/pds/package.json | 1 + packages/pds/src/app-view/services/indexing/util.ts | 9 ++++++++- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/bsky/package.json b/packages/bsky/package.json index c2a5a756864..5d3d1ea77d3 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -48,6 +48,7 @@ "http-errors": "^2.0.0", "http-terminator": "^3.2.0", "ioredis": "^5.3.2", + "iso-datestring-validator": "^2.2.2", "kysely": "^0.22.0", "multiformats": "^9.6.4", "p-queue": "^6.6.2", diff --git a/packages/bsky/src/services/indexing/util.ts b/packages/bsky/src/services/indexing/util.ts index d0a5755d589..5ce78959f8f 100644 --- a/packages/bsky/src/services/indexing/util.ts +++ b/packages/bsky/src/services/indexing/util.ts @@ -1,3 +1,5 @@ +import { isValidISODateString } from 'iso-datestring-validator' + // Normalize date strings to simplified ISO so that the lexical sort preserves temporal sort. // Rather than failing on an invalid date format, returns valid unix epoch. export function toSimplifiedISOSafe(dateStr: string) { @@ -5,5 +7,10 @@ export function toSimplifiedISOSafe(dateStr: string) { if (isNaN(date.getTime())) { return new Date(0).toISOString() } - return date.toISOString() // YYYY-MM-DDTHH:mm:ss.sssZ + const iso = date.toISOString() + if (!isValidISODateString(iso)) { + // Occurs in rare cases, e.g. where resulting UTC year is negative. These also don't preserve lexical sort. + return new Date(0).toISOString() + } + return iso // YYYY-MM-DDTHH:mm:ss.sssZ } diff --git a/packages/pds/package.json b/packages/pds/package.json index 8442531b63c..5e0e12e8235 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -55,6 +55,7 @@ "http-errors": "^2.0.0", "http-terminator": "^3.2.0", "ioredis": "^5.3.2", + "iso-datestring-validator": "^2.2.2", "jsonwebtoken": "^8.5.1", "kysely": "^0.22.0", "lru-cache": "^10.0.1", diff --git a/packages/pds/src/app-view/services/indexing/util.ts b/packages/pds/src/app-view/services/indexing/util.ts index d0a5755d589..5ce78959f8f 100644 --- a/packages/pds/src/app-view/services/indexing/util.ts +++ b/packages/pds/src/app-view/services/indexing/util.ts @@ -1,3 +1,5 @@ +import { isValidISODateString } from 'iso-datestring-validator' + // Normalize date strings to simplified ISO so that the lexical sort preserves temporal sort. // Rather than failing on an invalid date format, returns valid unix epoch. export function toSimplifiedISOSafe(dateStr: string) { @@ -5,5 +7,10 @@ export function toSimplifiedISOSafe(dateStr: string) { if (isNaN(date.getTime())) { return new Date(0).toISOString() } - return date.toISOString() // YYYY-MM-DDTHH:mm:ss.sssZ + const iso = date.toISOString() + if (!isValidISODateString(iso)) { + // Occurs in rare cases, e.g. where resulting UTC year is negative. These also don't preserve lexical sort. + return new Date(0).toISOString() + } + return iso // YYYY-MM-DDTHH:mm:ss.sssZ } From bbc6a2c0fd7e2ecbafcb4006eeeb24c657a4a808 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 23 Aug 2023 11:19:47 -0500 Subject: [PATCH 183/237] Indexer request reprocess (#1504) * allow requesting reprocessing a repo from an indexer * partition id guardrailes * test * build branch * dont set ports * resupply port * give port in correct spot * dont build branch --- packages/bsky/src/indexer/config.ts | 15 ++++ packages/bsky/src/indexer/index.ts | 18 ++++- packages/bsky/src/indexer/server.ts | 46 ++++++++++++ packages/bsky/src/indexer/subscription.ts | 10 +++ packages/bsky/src/services/indexing/index.ts | 2 +- packages/bsky/tests/reprocessing.test.ts | 75 ++++++++++++++++++++ packages/dev-env/src/bsky.ts | 22 +++--- 7 files changed, 177 insertions(+), 11 deletions(-) create mode 100644 packages/bsky/src/indexer/server.ts create mode 100644 packages/bsky/tests/reprocessing.test.ts diff --git a/packages/bsky/src/indexer/config.ts b/packages/bsky/src/indexer/config.ts index d78a0bce93a..32f599a8d1e 100644 --- a/packages/bsky/src/indexer/config.ts +++ b/packages/bsky/src/indexer/config.ts @@ -21,6 +21,8 @@ export interface IndexerConfigValues { indexerPartitionIds: number[] indexerPartitionBatchSize?: number indexerSubLockId?: number + indexerPort?: number + ingesterPartitionCount: number indexerNamespace?: string } @@ -75,6 +77,9 @@ export class IndexerConfig { const indexerConcurrency = maybeParseInt(process.env.INDEXER_CONCURRENCY) const indexerNamespace = overrides?.indexerNamespace const indexerSubLockId = maybeParseInt(process.env.INDEXER_SUB_LOCK_ID) + const indexerPort = maybeParseInt(process.env.INDEXER_PORT) + const ingesterPartitionCount = + maybeParseInt(process.env.INGESTER_PARTITION_COUNT) ?? 64 const labelerKeywords = {} assert(dbPostgresUrl) assert(redisHost || (redisSentinelName && redisSentinelHosts?.length)) @@ -99,6 +104,8 @@ export class IndexerConfig { indexerPartitionBatchSize, indexerNamespace, indexerSubLockId, + indexerPort, + ingesterPartitionCount, labelerKeywords, ...stripUndefineds(overrides ?? {}), }) @@ -180,6 +187,14 @@ export class IndexerConfig { return this.cfg.indexerSubLockId } + get indexerPort() { + return this.cfg.indexerPort + } + + get ingesterPartitionCount() { + return this.cfg.ingesterPartitionCount + } + get labelerKeywords() { return this.cfg.labelerKeywords } diff --git a/packages/bsky/src/indexer/index.ts b/packages/bsky/src/indexer/index.ts index 8bd21120fab..13ee2e9f0cb 100644 --- a/packages/bsky/src/indexer/index.ts +++ b/packages/bsky/src/indexer/index.ts @@ -1,3 +1,4 @@ +import express from 'express' import { IdResolver } from '@atproto/identity' import { BackgroundQueue } from '../background' import { PrimaryDatabase } from '../db' @@ -10,6 +11,7 @@ import { createServices } from './services' import { IndexerSubscription } from './subscription' import { HiveLabeler, KeywordLabeler, Labeler } from '../labeler' import { Redis } from '../redis' +import { CloseFn, createServer, startServer } from './server' export { IndexerConfig } from './config' export type { IndexerConfigValues } from './config' @@ -17,12 +19,19 @@ export type { IndexerConfigValues } from './config' export class BskyIndexer { public ctx: IndexerContext public sub: IndexerSubscription + public app: express.Application + private closeServer?: CloseFn private dbStatsInterval: NodeJS.Timer private subStatsInterval: NodeJS.Timer - constructor(opts: { ctx: IndexerContext; sub: IndexerSubscription }) { + constructor(opts: { + ctx: IndexerContext + sub: IndexerSubscription + app: express.Application + }) { this.ctx = opts.ctx this.sub = opts.sub + this.app = opts.app } static create(opts: { @@ -74,7 +83,10 @@ export class BskyIndexer { concurrency: cfg.indexerConcurrency, subLockId: cfg.indexerSubLockId, }) - return new BskyIndexer({ ctx, sub }) + + const app = createServer(sub, cfg) + + return new BskyIndexer({ ctx, sub, app }) } async start() { @@ -108,10 +120,12 @@ export class BskyIndexer { ) }, 500) this.sub.run() + this.closeServer = startServer(this.app, this.ctx.cfg.indexerPort) return this } async destroy(opts?: { skipDb: boolean; skipRedis: true }): Promise { + if (this.closeServer) await this.closeServer() await this.sub.destroy() clearInterval(this.subStatsInterval) if (!opts?.skipRedis) await this.ctx.redis.destroy() diff --git a/packages/bsky/src/indexer/server.ts b/packages/bsky/src/indexer/server.ts new file mode 100644 index 00000000000..1dfdd1a1ddb --- /dev/null +++ b/packages/bsky/src/indexer/server.ts @@ -0,0 +1,46 @@ +import express from 'express' +import { IndexerSubscription } from './subscription' +import { IndexerConfig } from './config' +import { randomIntFromSeed } from '@atproto/crypto' + +export type CloseFn = () => Promise + +export const createServer = ( + sub: IndexerSubscription, + cfg: IndexerConfig, +): express.Application => { + const app = express() + app.post('/reprocess/:did', async (req, res) => { + const did = req.params.did + try { + const partition = await randomIntFromSeed(did, cfg.ingesterPartitionCount) + const supportedPartition = cfg.indexerPartitionIds.includes(partition) + if (!supportedPartition) { + return res.status(400).send(`unsupported partition: ${partition}`) + } + } catch (err) { + return res.status(500).send('could not calculate partition') + } + sub.requestReprocess(req.params.did) + res.sendStatus(200) + }) + return app +} + +export const startServer = ( + app: express.Application, + port?: number, +): CloseFn => { + const server = app.listen(port) + return () => { + return new Promise((resolve, reject) => { + server.close((err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + } +} diff --git a/packages/bsky/src/indexer/subscription.ts b/packages/bsky/src/indexer/subscription.ts index e3914571bd4..76262f8a82e 100644 --- a/packages/bsky/src/indexer/subscription.ts +++ b/packages/bsky/src/indexer/subscription.ts @@ -106,6 +106,16 @@ export class IndexerSubscription { } } + requestReprocess(did: string) { + this.repoQueue.add(did, async () => { + try { + await this.indexingSvc.indexRepo(did) + } catch (err) { + log.error({ did }, 'failed to reprocess repo') + } + }) + } + async destroy() { this.destroyed = true await this.repoQueue.destroy() diff --git a/packages/bsky/src/services/indexing/index.ts b/packages/bsky/src/services/indexing/index.ts index cf30510cbe4..e45cf25967f 100644 --- a/packages/bsky/src/services/indexing/index.ts +++ b/packages/bsky/src/services/indexing/index.ts @@ -157,7 +157,7 @@ export class IndexingService { .executeTakeFirst() } - async indexRepo(did: string, commit: string) { + async indexRepo(did: string, commit?: string) { this.db.assertNotTransaction() const now = new Date().toISOString() const { pds, signingKey } = await this.idResolver.did.resolveAtprotoData( diff --git a/packages/bsky/tests/reprocessing.test.ts b/packages/bsky/tests/reprocessing.test.ts new file mode 100644 index 00000000000..b45f252df07 --- /dev/null +++ b/packages/bsky/tests/reprocessing.test.ts @@ -0,0 +1,75 @@ +import axios from 'axios' +import { AtUri } from '@atproto/uri' +import AtpAgent from '@atproto/api' +import { TestNetwork } from '@atproto/dev-env' +import { SeedClient } from './seeds/client' +import basicSeed from './seeds/basic' +import { Database } from '../src/db' + +describe('reprocessing', () => { + let network: TestNetwork + let pdsAgent: AtpAgent + let sc: SeedClient + let alice: string + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_reprocessing', + }) + pdsAgent = network.pds.getClient() + sc = new SeedClient(pdsAgent) + await basicSeed(sc) + alice = sc.dids.alice + await network.processAll() + }) + + afterAll(async () => { + await network.close() + }) + + const getRecordUris = async (db: Database, did: string) => { + const res = await db.db + .selectFrom('record') + .select('uri') + .where('did', '=', did) + .execute() + return res.map((row) => row.uri) + } + it('reprocesses repo data', async () => { + const db = network.bsky.ctx.db.getPrimary() + const urisBefore = await getRecordUris(db, alice) + await db.db.deleteFrom('record').where('did', '=', alice).execute() + const indexerPort = network.bsky.indexer.ctx.cfg.indexerPort + await axios.post(`http://localhost:${indexerPort}/reprocess/${alice}`) + await network.processAll() + const urisAfter = await getRecordUris(db, alice) + expect(urisAfter.sort()).toEqual(urisBefore.sort()) + }) + + it('buffers commits while reprocessing repo data', async () => { + const db = network.bsky.ctx.db.getPrimary() + const urisBefore = await getRecordUris(db, alice) + await db.db.deleteFrom('record').where('did', '=', alice).execute() + const indexerPort = network.bsky.indexer.ctx.cfg.indexerPort + const toDeleteIndex = urisBefore.findIndex((uri) => + uri.includes('app.bsky.feed.post'), + ) + if (toDeleteIndex < 0) { + throw new Error('could not find post to delete') + } + // request reprocess while buffering a new post & delete + const [newPost] = await Promise.all([ + sc.post(alice, 'blah blah'), + axios.post(`http://localhost:${indexerPort}/reprocess/${alice}`), + sc.deletePost(alice, new AtUri(urisBefore[toDeleteIndex])), + ]) + await network.processAll() + const urisAfter = await getRecordUris(db, alice) + const expected = [ + ...urisBefore.slice(0, toDeleteIndex), + ...urisBefore.slice(toDeleteIndex + 1), + newPost.ref.uriStr, + ] + expect(urisAfter.sort()).toEqual(expected.sort()) + }) +}) diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index 8445881244e..d0a85d1f3b3 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -95,6 +95,8 @@ export class TestBsky { indexerPartitionIds: [0], indexerNamespace: `ns${ns}`, indexerSubLockId: uniqueLockId(), + indexerPort: await getPort(), + ingesterPartitionCount: 1, }) assert(indexerCfg.redisHost) const indexerRedis = new bsky.Redis({ @@ -236,6 +238,7 @@ export async function getIndexers( didPlcUrl: network.plc.url, indexerPartitionIds: [0], indexerNamespace: `ns${ns}`, + ingesterPartitionCount: config.ingesterPartitionCount ?? 1, ...config, } const db = new bsky.PrimaryDatabase({ @@ -247,14 +250,17 @@ export async function getIndexers( host: baseCfg.redisHost, namespace: baseCfg.indexerNamespace, }) - const indexers = opts.partitionIdsByIndexer.map((indexerPartitionIds) => { - const cfg = new bsky.IndexerConfig({ - ...baseCfg, - indexerPartitionIds, - indexerSubLockId: uniqueLockId(), - }) - return bsky.BskyIndexer.create({ cfg, db, redis }) - }) + const indexers = await Promise.all( + opts.partitionIdsByIndexer.map(async (indexerPartitionIds) => { + const cfg = new bsky.IndexerConfig({ + ...baseCfg, + indexerPartitionIds, + indexerSubLockId: uniqueLockId(), + indexerPort: await getPort(), + }) + return bsky.BskyIndexer.create({ cfg, db, redis }) + }), + ) await db.migrateToLatestOrThrow() return { db, From eae682f3bf7d18436a208e2cd78cef6e5f014c7a Mon Sep 17 00:00:00 2001 From: Ansh Date: Wed, 23 Aug 2023 15:37:14 -0700 Subject: [PATCH 184/237] Native Notifications (#1428) * pseudocode for sending notification to user * add notification push token table * lexicon codegen * `pds` and `api` codegen * update lexicon * add simple function to `putNotificationPushToken` into `notification_push_token` table * reorgnaize imports * add unspecced `putNotificationPushToken` to pds * add `notification-push-tokens` table to PDS * check if token exists before adding it to db * add endpoint and appId to PDS table * setup notification server * fix logic for inserting token into db * fix NotificationServer methods by making them static * fix merge conflicts * add comments on how sending ntoifications work * remove dead code * move notifServer to AppContext * refactor code to handle notif sending in `Indexer` service * add additional data when sending notifs * clean up code * move notif logic to indexer * add `appId` and `endpoint` optional params to putNotificationPushToken * clone notification code to AppView * add endpoint to register push token with app view * disable pds and enable app view notification server * clean up code * simply logic to check if a token already exists * remove NotificationServer from PDS * remove notification-push-token table from PDS * remove `putNotificationPushToken` endpoint * clean up code * let `axios` throw error if `gorush` has an error * let `kysely` throw error if notif cannot be registered by client * rename `registerPushNotificationEndpoint` to `registerPushNotifications` * delete `putNotificationPushToken` from AppView * rename putPushNotificationToken to registerPushNotification * remove dead notification code from pds * remove sanitizeDisplayName from NotificationSever * move `pushNotificationEndpoint` to config * temp add `pushNotificationEndpoint` to dev-env setup * remove example test from feed-generation.test.ts * add test for registerPushNotification and clean up error handling * move notifications test to its own file * add test for NotificationServer to check if tokens are retrieved correctly * remove unused functions from NotificationServer * add additional tests for NotificationServer * add return type to getNotificationDisplayAttributes function * remove unnecessary comments * remove dead NotificationServer code from PDS * clean up code to prepareNotifsToSend * put sending notifications as part of the backgroundQueue * log instead of throwing error if notification attributes don't exist * remove logs * add more tests to `notification-server.test.ts` * show replied with text for reply notifs * better error handling when sending notifications via backgroundQueue * add rate limit and batching to sending notifications * add comments to NotificationServer * merge with main * use redis for rate limiting instead of normal rate limits * move `notificationBatchSize` into config * usePrimary db in test * hoist push notif migration to present, update model to remove endpoint * update push notif lexicon * pare down lex for unregistering push * helpers for working with service endpoints from did doc * service-authed register/unregister push notifs * add well-known endpoint to appview * update bsky notif service tests * fix to batching logic, misc tidy * batch display notifications * colocate all notification sending logic * tidy tests * remove unregister token for now * fix registerPush lexicon, make a procedure * fix registerPush impl, test pds proxy to notif service * fix tests, make notif server optional when not configured * fix notif server config for bsky app service * move notif server rate limiting in-mem for now, add sending retry * codegen tidy * only push notifs on commit * build * fix notif rate limiter check * send notifs from users w/o a profile * remove build --------- Co-authored-by: dholms Co-authored-by: Devin Ivy --- .../app/bsky/notification/registerPush.json | 23 ++ packages/api/src/client/index.ts | 13 + packages/api/src/client/lexicons.ts | 36 ++ .../app/bsky/notification/registerPush.ts | 35 ++ .../types/com/atproto/sync/subscribeRepos.ts | 1 + packages/bsky/package.json | 1 + .../api/app/bsky/notification/registerPush.ts | 31 ++ packages/bsky/src/api/index.ts | 4 + packages/bsky/src/api/well-known.ts | 26 ++ packages/bsky/src/auth.ts | 2 +- packages/bsky/src/context.ts | 6 + packages/bsky/src/db/database-schema.ts | 2 + ...0230817T195936007Z-native-notifications.ts | 16 + packages/bsky/src/db/migrations/index.ts | 1 + .../src/db/tables/notification-push-token.ts | 10 + packages/bsky/src/index.ts | 6 +- packages/bsky/src/indexer/config.ts | 7 + packages/bsky/src/indexer/index.ts | 11 +- packages/bsky/src/indexer/services.ts | 11 +- packages/bsky/src/lexicon/index.ts | 12 + packages/bsky/src/lexicon/lexicons.ts | 36 ++ .../app/bsky/notification/registerPush.ts | 41 +++ .../types/com/atproto/sync/subscribeRepos.ts | 1 + packages/bsky/src/notifications.ts | 315 ++++++++++++++++++ packages/bsky/src/redis.ts | 16 + packages/bsky/src/services/indexing/index.ts | 28 +- .../src/services/indexing/plugins/block.ts | 4 +- .../indexing/plugins/feed-generator.ts | 4 +- .../src/services/indexing/plugins/follow.ts | 4 +- .../src/services/indexing/plugins/like.ts | 4 +- .../services/indexing/plugins/list-item.ts | 4 +- .../src/services/indexing/plugins/list.ts | 4 +- .../src/services/indexing/plugins/post.ts | 4 +- .../src/services/indexing/plugins/profile.ts | 4 +- .../src/services/indexing/plugins/repost.ts | 4 +- .../bsky/src/services/indexing/processor.ts | 25 ++ .../bsky/tests/notification-server.test.ts | 146 ++++++++ packages/dev-env/src/bsky.ts | 1 + packages/identity/src/did/atproto-data.ts | 69 ++-- .../src/app-view/api/app/bsky/feed/getFeed.ts | 23 +- .../pds/src/app-view/api/app/bsky/index.ts | 2 + .../api/app/bsky/notification/registerPush.ts | 47 +++ .../app-view/api/app/bsky/util/resolver.ts | 20 ++ packages/pds/src/lexicon/index.ts | 12 + packages/pds/src/lexicon/lexicons.ts | 36 ++ .../app/bsky/notification/registerPush.ts | 41 +++ .../types/com/atproto/sync/subscribeRepos.ts | 1 + packages/pds/tests/proxied/notif.test.ts | 91 +++++ yarn.lock | 5 + 49 files changed, 1171 insertions(+), 75 deletions(-) create mode 100644 lexicons/app/bsky/notification/registerPush.json create mode 100644 packages/api/src/client/types/app/bsky/notification/registerPush.ts create mode 100644 packages/bsky/src/api/app/bsky/notification/registerPush.ts create mode 100644 packages/bsky/src/api/well-known.ts create mode 100644 packages/bsky/src/db/migrations/20230817T195936007Z-native-notifications.ts create mode 100644 packages/bsky/src/db/tables/notification-push-token.ts create mode 100644 packages/bsky/src/lexicon/types/app/bsky/notification/registerPush.ts create mode 100644 packages/bsky/src/notifications.ts create mode 100644 packages/bsky/tests/notification-server.test.ts create mode 100644 packages/pds/src/app-view/api/app/bsky/notification/registerPush.ts create mode 100644 packages/pds/src/app-view/api/app/bsky/util/resolver.ts create mode 100644 packages/pds/src/lexicon/types/app/bsky/notification/registerPush.ts create mode 100644 packages/pds/tests/proxied/notif.test.ts diff --git a/lexicons/app/bsky/notification/registerPush.json b/lexicons/app/bsky/notification/registerPush.json new file mode 100644 index 00000000000..af83cfaf89c --- /dev/null +++ b/lexicons/app/bsky/notification/registerPush.json @@ -0,0 +1,23 @@ +{ + "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"} + } + } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 4a7b9081321..94bb818d31a 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -121,6 +121,7 @@ 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 AppBskyRichtextFacet from './types/app/bsky/richtext/facet' import * as AppBskyUnspeccedApplyLabels from './types/app/bsky/unspecced/applyLabels' @@ -242,6 +243,7 @@ export * as AppBskyGraphUnmuteActor from './types/app/bsky/graph/unmuteActor' export * as AppBskyGraphUnmuteActorList from './types/app/bsky/graph/unmuteActorList' export * as AppBskyNotificationGetUnreadCount from './types/app/bsky/notification/getUnreadCount' export * as AppBskyNotificationListNotifications from './types/app/bsky/notification/listNotifications' +export * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush' export * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen' export * as AppBskyRichtextFacet from './types/app/bsky/richtext/facet' export * as AppBskyUnspeccedApplyLabels from './types/app/bsky/unspecced/applyLabels' @@ -2024,6 +2026,17 @@ export class NotificationNS { }) } + registerPush( + data?: AppBskyNotificationRegisterPush.InputSchema, + opts?: AppBskyNotificationRegisterPush.CallOptions, + ): Promise { + return this._service.xrpc + .call('app.bsky.notification.registerPush', opts?.qp, data, opts) + .catch((e) => { + throw AppBskyNotificationRegisterPush.toKnownErr(e) + }) + } + updateSeen( data?: AppBskyNotificationUpdateSeen.InputSchema, opts?: AppBskyNotificationUpdateSeen.CallOptions, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index ab7e2febe1b..b5c82ce2f73 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -3575,6 +3575,8 @@ export const schemaDict = { }, 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: { @@ -6367,6 +6369,39 @@ export const schemaDict = { }, }, }, + 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', @@ -6746,6 +6781,7 @@ export const ids = { 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', AppBskyUnspeccedApplyLabels: 'app.bsky.unspecced.applyLabels', diff --git a/packages/api/src/client/types/app/bsky/notification/registerPush.ts b/packages/api/src/client/types/app/bsky/notification/registerPush.ts new file mode 100644 index 00000000000..9354ef76766 --- /dev/null +++ b/packages/api/src/client/types/app/bsky/notification/registerPush.ts @@ -0,0 +1,35 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface QueryParams {} + +export interface InputSchema { + serviceDid: string + token: string + platform: 'ios' | 'android' | 'web' | (string & {}) + appId: string + [k: string]: unknown +} + +export interface CallOptions { + headers?: Headers + qp?: QueryParams + encoding: 'application/json' +} + +export interface Response { + success: boolean + headers: Headers +} + +export function toKnownErr(e: any) { + if (e instanceof XRPCError) { + } + return e +} diff --git a/packages/api/src/client/types/com/atproto/sync/subscribeRepos.ts b/packages/api/src/client/types/com/atproto/sync/subscribeRepos.ts index 7e41515badb..306f5c815f7 100644 --- a/packages/api/src/client/types/com/atproto/sync/subscribeRepos.ts +++ b/packages/api/src/client/types/com/atproto/sync/subscribeRepos.ts @@ -111,6 +111,7 @@ 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 diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 5d3d1ea77d3..15d8e7e7644 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -40,6 +40,7 @@ "@atproto/uri": "*", "@atproto/xrpc-server": "*", "@did-plc/lib": "^0.0.1", + "@isaacs/ttlcache": "^1.4.1", "compression": "^1.7.4", "cors": "^2.8.5", "dotenv": "^16.0.0", diff --git a/packages/bsky/src/api/app/bsky/notification/registerPush.ts b/packages/bsky/src/api/app/bsky/notification/registerPush.ts new file mode 100644 index 00000000000..be7d373bcd4 --- /dev/null +++ b/packages/bsky/src/api/app/bsky/notification/registerPush.ts @@ -0,0 +1,31 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { Platform } from '../../../../notifications' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.notification.registerPush({ + auth: ctx.authVerifier, + handler: async ({ auth, input }) => { + const { token, platform, serviceDid, appId } = input.body + const { + credentials: { did }, + } = auth + if (serviceDid !== auth.artifacts.aud) { + throw new InvalidRequestError('Invalid serviceDid.') + } + const { notifServer } = ctx + if (platform !== 'ios' && platform !== 'android' && platform !== 'web') { + throw new InvalidRequestError( + 'Unsupported platform: must be "ios", "android", or "web".', + ) + } + await notifServer.registerDeviceForPushNotifications( + did, + token, + platform as Platform, + appId, + ) + }, + }) +} diff --git a/packages/bsky/src/api/index.ts b/packages/bsky/src/api/index.ts index 6ecf3495783..54cbcd6e02e 100644 --- a/packages/bsky/src/api/index.ts +++ b/packages/bsky/src/api/index.ts @@ -32,6 +32,7 @@ import getSuggestions from './app/bsky/actor/getSuggestions' import getUnreadCount from './app/bsky/notification/getUnreadCount' import listNotifications from './app/bsky/notification/listNotifications' 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' @@ -50,6 +51,8 @@ import getRecord from './com/atproto/repo/getRecord' export * as health from './health' +export * as wellKnown from './well-known' + export * as blobResolver from './blob-resolver' export default function (server: Server, ctx: AppContext) { @@ -86,6 +89,7 @@ export default function (server: Server, ctx: AppContext) { getUnreadCount(server, ctx) listNotifications(server, ctx) updateSeen(server, ctx) + registerPush(server, ctx) getPopularFeedGenerators(server, ctx) getTimelineSkeleton(server, ctx) // com.atproto diff --git a/packages/bsky/src/api/well-known.ts b/packages/bsky/src/api/well-known.ts new file mode 100644 index 00000000000..b6813751605 --- /dev/null +++ b/packages/bsky/src/api/well-known.ts @@ -0,0 +1,26 @@ +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.publicUrl && new URL(ctx.cfg.publicUrl).hostname + if (!hostname || ctx.cfg.serverDid !== `did:web:${hostname}`) { + return res.sendStatus(404) + } + res.json({ + '@context': ['https://www.w3.org/ns/did/v1'], + id: ctx.cfg.serverDid, + service: [ + { + id: '#bsky_notif', + type: 'BskyNotificationService', + serviceEndpoint: `https://${hostname}`, + }, + ], + }) + }) + + return router +} diff --git a/packages/bsky/src/auth.ts b/packages/bsky/src/auth.ts index a90cf5fa410..a92023d55f5 100644 --- a/packages/bsky/src/auth.ts +++ b/packages/bsky/src/auth.ts @@ -18,7 +18,7 @@ export const authVerifier = const atprotoData = await idResolver.did.resolveAtprotoData(did) return atprotoData.signingKey }) - return { credentials: { did } } + return { credentials: { did }, artifacts: { aud: opts.aud } } } export const authOptionalVerifier = diff --git a/packages/bsky/src/context.ts b/packages/bsky/src/context.ts index a81aa8eab5a..343d105ce1a 100644 --- a/packages/bsky/src/context.ts +++ b/packages/bsky/src/context.ts @@ -9,6 +9,7 @@ import DidSqlCache from './did-cache' import { BackgroundQueue } from './background' import { MountedAlgos } from './feed-gen/types' import { LabelCache } from './label-cache' +import { NotificationServer } from './notifications' export class AppContext { constructor( @@ -22,6 +23,7 @@ export class AppContext { labelCache: LabelCache backgroundQueue: BackgroundQueue algos: MountedAlgos + notifServer: NotificationServer }, ) {} @@ -57,6 +59,10 @@ export class AppContext { return this.opts.labelCache } + get notifServer(): NotificationServer { + return this.opts.notifServer + } + get authVerifier() { return auth.authVerifier(this.idResolver, { aud: this.cfg.serverDid }) } diff --git a/packages/bsky/src/db/database-schema.ts b/packages/bsky/src/db/database-schema.ts index 16547eb94df..9c6d0beaa71 100644 --- a/packages/bsky/src/db/database-schema.ts +++ b/packages/bsky/src/db/database-schema.ts @@ -21,6 +21,7 @@ import * as actorState from './tables/actor-state' import * as actorSync from './tables/actor-sync' import * as record from './tables/record' import * as notification from './tables/notification' +import * as notificationPushToken from './tables/notification-push-token' import * as didCache from './tables/did-cache' import * as moderation from './tables/moderation' import * as label from './tables/label' @@ -50,6 +51,7 @@ export type DatabaseSchemaType = duplicateRecord.PartialDB & actorSync.PartialDB & record.PartialDB & notification.PartialDB & + notificationPushToken.PartialDB & didCache.PartialDB & moderation.PartialDB & label.PartialDB & diff --git a/packages/bsky/src/db/migrations/20230817T195936007Z-native-notifications.ts b/packages/bsky/src/db/migrations/20230817T195936007Z-native-notifications.ts new file mode 100644 index 00000000000..22cda5b78a4 --- /dev/null +++ b/packages/bsky/src/db/migrations/20230817T195936007Z-native-notifications.ts @@ -0,0 +1,16 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('notification_push_token') + .addColumn('did', 'varchar', (col) => col.notNull()) + .addColumn('platform', 'varchar', (col) => col.notNull()) + .addColumn('token', 'varchar', (col) => col.notNull()) + .addColumn('appId', 'varchar', (col) => col.notNull()) + .addPrimaryKeyConstraint('notification_push_token_pkey', ['did', 'token']) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('notification_push_token').execute() +} diff --git a/packages/bsky/src/db/migrations/index.ts b/packages/bsky/src/db/migrations/index.ts index 044ec31f8d7..5bb0497582b 100644 --- a/packages/bsky/src/db/migrations/index.ts +++ b/packages/bsky/src/db/migrations/index.ts @@ -24,3 +24,4 @@ export * as _20230720T164800037Z from './20230720T164800037Z-posts-cursor-idx' export * as _20230807T035309811Z from './20230807T035309811Z-feed-item-delete-invite-for-user-idx' export * as _20230808T172902639Z from './20230808T172902639Z-repo-rev' export * as _20230810T203349843Z from './20230810T203349843Z-action-duration' +export * as _20230817T195936007Z from './20230817T195936007Z-native-notifications' diff --git a/packages/bsky/src/db/tables/notification-push-token.ts b/packages/bsky/src/db/tables/notification-push-token.ts new file mode 100644 index 00000000000..36a82fdddfe --- /dev/null +++ b/packages/bsky/src/db/tables/notification-push-token.ts @@ -0,0 +1,10 @@ +export const tableName = 'notification_push_token' + +export interface NotificationPushToken { + did: string + platform: 'ios' | 'android' | 'web' + token: string + appId: string +} + +export type PartialDB = { [tableName]: NotificationPushToken } diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 7b00e0d73c6..b469c2f7b17 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -6,7 +6,7 @@ import { createHttpTerminator, HttpTerminator } from 'http-terminator' import cors from 'cors' import compression from 'compression' import { IdResolver } from '@atproto/identity' -import API, { health, blobResolver } from './api' +import API, { health, wellKnown, blobResolver } from './api' import { DatabaseCoordinator } from './db' import * as error from './error' import { dbLogger, loggerMiddleware } from './logger' @@ -24,6 +24,7 @@ import { import { BackgroundQueue } from './background' import { MountedAlgos } from './feed-gen/types' import { LabelCache } from './label-cache' +import { NotificationServer } from './notifications' export type { ServerConfigValues } from './config' export type { MountedAlgos } from './feed-gen/types' @@ -98,6 +99,7 @@ export class BskyAppView { const backgroundQueue = new BackgroundQueue(db.getPrimary()) const labelCache = new LabelCache(db.getPrimary()) + const notifServer = new NotificationServer(db.getPrimary()) const services = createServices({ imgUriBuilder, @@ -115,6 +117,7 @@ export class BskyAppView { labelCache, backgroundQueue, algos, + notifServer, }) let server = createServer({ @@ -129,6 +132,7 @@ export class BskyAppView { server = API(server, ctx) app.use(health.createRouter(ctx)) + app.use(wellKnown.createRouter(ctx)) app.use(blobResolver.createRouter(ctx)) if (imgProcessingServer) { app.use('/img', imgProcessingServer.app) diff --git a/packages/bsky/src/indexer/config.ts b/packages/bsky/src/indexer/config.ts index 32f599a8d1e..37c04af6cb3 100644 --- a/packages/bsky/src/indexer/config.ts +++ b/packages/bsky/src/indexer/config.ts @@ -24,6 +24,7 @@ export interface IndexerConfigValues { indexerPort?: number ingesterPartitionCount: number indexerNamespace?: string + pushNotificationEndpoint?: string } export class IndexerConfig { @@ -81,6 +82,7 @@ export class IndexerConfig { const ingesterPartitionCount = maybeParseInt(process.env.INGESTER_PARTITION_COUNT) ?? 64 const labelerKeywords = {} + const pushNotificationEndpoint = process.env.PUSH_NOTIFICATION_ENDPOINT assert(dbPostgresUrl) assert(redisHost || (redisSentinelName && redisSentinelHosts?.length)) assert(indexerPartitionIds.length > 0) @@ -107,6 +109,7 @@ export class IndexerConfig { indexerPort, ingesterPartitionCount, labelerKeywords, + pushNotificationEndpoint, ...stripUndefineds(overrides ?? {}), }) } @@ -198,6 +201,10 @@ export class IndexerConfig { get labelerKeywords() { return this.cfg.labelerKeywords } + + get pushNotificationEndpoint() { + return this.cfg.pushNotificationEndpoint + } } function stripUndefineds( diff --git a/packages/bsky/src/indexer/index.ts b/packages/bsky/src/indexer/index.ts index 13ee2e9f0cb..3e955b53351 100644 --- a/packages/bsky/src/indexer/index.ts +++ b/packages/bsky/src/indexer/index.ts @@ -11,6 +11,7 @@ import { createServices } from './services' import { IndexerSubscription } from './subscription' import { HiveLabeler, KeywordLabeler, Labeler } from '../labeler' import { Redis } from '../redis' +import { NotificationServer } from '../notifications' import { CloseFn, createServer, startServer } from './server' export { IndexerConfig } from './config' @@ -67,7 +68,15 @@ export class BskyIndexer { backgroundQueue, }) } - const services = createServices({ idResolver, labeler, backgroundQueue }) + const notifServer = cfg.pushNotificationEndpoint + ? new NotificationServer(db, cfg.pushNotificationEndpoint) + : undefined + const services = createServices({ + idResolver, + labeler, + backgroundQueue, + notifServer, + }) const ctx = new IndexerContext({ db, redis, diff --git a/packages/bsky/src/indexer/services.ts b/packages/bsky/src/indexer/services.ts index 362fed513d0..0e6908de099 100644 --- a/packages/bsky/src/indexer/services.ts +++ b/packages/bsky/src/indexer/services.ts @@ -4,15 +4,22 @@ import { Labeler } from '../labeler' import { BackgroundQueue } from '../background' import { IndexingService } from '../services/indexing' import { LabelService } from '../services/label' +import { NotificationServer } from '../notifications' export function createServices(resources: { idResolver: IdResolver labeler: Labeler backgroundQueue: BackgroundQueue + notifServer?: NotificationServer }): Services { - const { idResolver, labeler, backgroundQueue } = resources + const { idResolver, labeler, backgroundQueue, notifServer } = resources return { - indexing: IndexingService.creator(idResolver, labeler, backgroundQueue), + indexing: IndexingService.creator( + idResolver, + labeler, + backgroundQueue, + notifServer, + ), label: LabelService.creator(null), } } diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 8b33f2c2c21..7143c1833a4 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -102,6 +102,7 @@ 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 AppBskyUnspeccedApplyLabels from './types/app/bsky/unspecced/applyLabels' import * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' @@ -1324,6 +1325,17 @@ export class NotificationNS { 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, diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index ab7e2febe1b..b5c82ce2f73 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -3575,6 +3575,8 @@ export const schemaDict = { }, 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: { @@ -6367,6 +6369,39 @@ export const schemaDict = { }, }, }, + 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', @@ -6746,6 +6781,7 @@ export const ids = { 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', AppBskyUnspeccedApplyLabels: 'app.bsky.unspecced.applyLabels', diff --git a/packages/bsky/src/lexicon/types/app/bsky/notification/registerPush.ts b/packages/bsky/src/lexicon/types/app/bsky/notification/registerPush.ts new file mode 100644 index 00000000000..9923aeb058e --- /dev/null +++ b/packages/bsky/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/bsky/src/lexicon/types/com/atproto/sync/subscribeRepos.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/subscribeRepos.ts index 9b5326ed347..7fafb749398 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/subscribeRepos.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/subscribeRepos.ts @@ -136,6 +136,7 @@ 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 diff --git a/packages/bsky/src/notifications.ts b/packages/bsky/src/notifications.ts new file mode 100644 index 00000000000..cdcb35f7684 --- /dev/null +++ b/packages/bsky/src/notifications.ts @@ -0,0 +1,315 @@ +import axios from 'axios' +import { Insertable } from 'kysely' +import TTLCache from '@isaacs/ttlcache' +import { AtUri } from '@atproto/api' +import { MINUTE, chunkArray } from '@atproto/common' +import Database from './db/primary' +import { Notification } from './db/tables/notification' +import { NotificationPushToken as PushToken } from './db/tables/notification-push-token' +import logger from './indexer/logger' +import { notSoftDeletedClause } from './db/util' +import { ids } from './lexicon/lexicons' +import { retryHttp } from './util/retry' + +export type Platform = 'ios' | 'android' | 'web' + +type PushNotification = { + tokens: string[] + platform: 1 | 2 // 1 = ios, 2 = android + title: string + message: string + topic: string + data?: { + [key: string]: string + } +} + +type InsertableNotif = Insertable + +type NotifDisplay = { + title: string + body: string + notif: InsertableNotif +} + +export class NotificationServer { + private rateLimiter = new RateLimiter(20, 20 * MINUTE) + + constructor(public db: Database, public pushEndpoint?: string) {} + + async getTokensByDid(dids: string[]) { + if (!dids.length) return {} + const tokens = await this.db.db + .selectFrom('notification_push_token') + .where('did', 'in', dids) + .selectAll() + .execute() + return tokens.reduce((acc, token) => { + acc[token.did] ??= [] + acc[token.did].push(token) + return acc + }, {} as Record) + } + + async prepareNotifsToSend(notifications: InsertableNotif[]) { + const notifsToSend: PushNotification[] = [] + const tokensByDid = await this.getTokensByDid( + unique(notifications.map((n) => n.did)), + ) + // views for all notifications that have tokens + const notificationViews = await this.getNotificationDisplayAttributes( + notifications.filter((n) => tokensByDid[n.did]), + ) + + for (const notifView of notificationViews) { + const { did: userDid } = notifView.notif + const userTokens = tokensByDid[userDid] ?? [] + for (const t of userTokens) { + const { appId, platform, token } = t + if (platform === 'ios' || platform === 'android') { + notifsToSend.push({ + tokens: [token], + platform: platform === 'ios' ? 1 : 2, + title: notifView.title, + message: notifView.body, + topic: appId, + data: { + reason: notifView.notif.reason, + recordUri: notifView.notif.recordUri, + recordCid: notifView.notif.recordCid, + }, + }) + } else { + // @TODO: Handle web notifs + logger.warn({ did: userDid }, 'cannot send web notification to user') + } + } + } + + return notifsToSend + } + + /** + * The function `addNotificationsToQueue` adds push notifications to a queue, taking into account rate + * limiting and batching the notifications for efficient processing. + * @param {PushNotification[]} notifs - An array of PushNotification objects. Each PushNotification + * object has a "tokens" property which is an array of tokens. + * @returns void + */ + async processNotifications(notifs: PushNotification[]) { + const now = Date.now() + const permittedNotifs = notifs.filter((n) => + n.tokens.every((token) => this.rateLimiter.check(token, now)), + ) + for (const batch of chunkArray(permittedNotifs, 20)) { + try { + await this.sendPushNotifications(batch) + } catch (err) { + logger.error({ err, batch }, 'notification push batch failed') + } + } + } + + /** 1. Get the user's token (APNS or FCM for iOS and Android respectively) from the database + User token will be in the format: + did || token || platform (1 = iOS, 2 = Android, 3 = Web) + 2. Send notification to `gorush` server with token + Notification will be in the format: + "notifications": [ + { + "tokens": string[], + "platform": 1 | 2, + "message": string, + "title": string, + "priority": "normal" | "high", + "image": string, (Android only) + "expiration": number, (iOS only) + "badge": number, (iOS only) + } + ] + 3. `gorush` will send notification to APNS or FCM + 4. store response from `gorush` which contains the ID of the notification + 5. If notification needs to be updated or deleted, find the ID of the notification from the database and send a new notification to `gorush` with the ID (repeat step 2) + */ + private async sendPushNotifications(notifications: PushNotification[]) { + // if pushEndpoint is not defined, we are not running in the indexer service, so we can't send push notifications + if (!this.pushEndpoint) { + throw new Error('Push endpoint not defined') + } + // if no notifications, skip and return early + if (notifications.length === 0) { + return + } + const pushEndpoint = this.pushEndpoint + await retryHttp(() => + axios.post( + pushEndpoint, + { notifications }, + { + headers: { + 'Content-Type': 'application/json', + accept: 'application/json', + }, + }, + ), + ) + } + + async registerDeviceForPushNotifications( + did: string, + token: string, + platform: Platform, + appId: string, + ) { + // if token doesn't exist, insert it, on conflict do nothing + await this.db.db + .insertInto('notification_push_token') + .values({ did, token, platform, appId }) + .onConflict((oc) => oc.doNothing()) + .execute() + } + + async getNotificationDisplayAttributes( + notifs: InsertableNotif[], + ): Promise { + const { ref } = this.db.db.dynamic + const authorDids = notifs.map((n) => n.author) + const subjectUris = notifs.flatMap((n) => n.reasonSubject ?? []) + const recordUris = notifs.map((n) => n.recordUri) + const allUris = [...subjectUris, ...recordUris] + + // gather potential display data for notifications in batch + const [authors, posts] = await Promise.all([ + this.db.db + .selectFrom('actor') + .leftJoin('profile', 'profile.creator', 'actor.did') + .leftJoin('record', 'record.uri', 'profile.uri') + .where(notSoftDeletedClause(ref('actor'))) + .where(notSoftDeletedClause(ref('record'))) + .where('profile.creator', 'in', authorDids.length ? authorDids : ['']) + .select(['actor.did as did', 'handle', 'displayName']) + .execute(), + this.db.db + .selectFrom('post') + .innerJoin('actor', 'actor.did', 'post.creator') + .innerJoin('record', 'record.uri', 'post.uri') + .where(notSoftDeletedClause(ref('actor'))) + .where(notSoftDeletedClause(ref('record'))) + .where('post.uri', 'in', allUris.length ? allUris : ['']) + .select(['post.uri as uri', 'text']) + .execute(), + ]) + + const authorsByDid = authors.reduce((acc, author) => { + acc[author.did] = author + return acc + }, {} as Record) + const postsByUri = posts.reduce((acc, post) => { + acc[post.uri] = post + return acc + }, {} as Record) + + const results: NotifDisplay[] = [] + + for (const notif of notifs) { + const { + author: authorDid, + reason, + reasonSubject: subjectUri, // if like/reply/quote/emtion, the post which was liked/replied to/mention is in/or quoted. if custom feed liked, the feed which was liked + recordUri, + } = notif + + const author = + authorsByDid[authorDid]?.displayName || authorsByDid[authorDid]?.handle + const postRecord = postsByUri[recordUri] + const postSubject = subjectUri ? postsByUri[subjectUri] : null + + // if no display name, dont send notification + if (!author) { + continue + } + // const author = displayName.displayName + + // 2. Get post data content + // if follow, get the URI of the author's profile + // if reply, or mention, get URI of the postRecord + // if like, or custom feed like, or repost get the URI of the reasonSubject + let title = '' + let body = '' + + // check follow first and mention first because they don't have subjectUri and return + // reply has subjectUri but the recordUri is the replied post + if (reason === 'follow') { + title = 'New follower!' + body = `${author} has followed you` + results.push({ title, body, notif }) + } else if (reason === 'mention' || reason === 'reply') { + // use recordUri for mention and reply + title = + reason === 'mention' + ? `${author} mentioned you` + : `${author} replied to your post` + body = postRecord?.text || '' + results.push({ title, body, notif }) + } + + // if no subjectUri, don't send notification + // at this point, subjectUri should exist for all the other reasons + if (!postSubject) { + continue + } + + if (reason === 'like') { + title = `${author} liked your post` + body = postSubject?.text || '' + // custom feed like + const uri = subjectUri ? new AtUri(subjectUri) : null + if (uri?.collection === ids.AppBskyFeedGenerator) { + title = `${author} liked your custom feed` + body = uri?.rkey ?? '' + } + } else if (reason === 'quote') { + title = `${author} quoted your post` + body = postSubject?.text || '' + } else if (reason === 'repost') { + title = `${author} reposted your post` + body = postSubject?.text || '' + } + + if (title === '' && body === '') { + logger.warn( + { notif }, + 'No notification display attributes found for this notification. Either profile or post data for this notification is missing.', + ) + continue + } + + results.push({ title, body, notif }) + } + + return results + } +} + +const unique = (items: string[]) => [...new Set(items)] + +class RateLimiter { + private rateLimitCache = new TTLCache({ + max: 50000, + ttl: this.windowMs, + noUpdateTTL: true, + }) + constructor(private limit: number, private windowMs: number) {} + check(token: string, now = Date.now()) { + const key = getRateLimitKey(token, now) + const last = this.rateLimitCache.get(key) ?? 0 + const current = last + 1 + this.rateLimitCache.set(key, current) + return current <= this.limit + } +} + +const getRateLimitKey = (token: string, now: number) => { + const iteration = Math.floor(now / (20 * MINUTE)) + return `${iteration}:${token}` +} diff --git a/packages/bsky/src/redis.ts b/packages/bsky/src/redis.ts index 2df5ff73768..72d895be24c 100644 --- a/packages/bsky/src/redis.ts +++ b/packages/bsky/src/redis.ts @@ -105,6 +105,22 @@ export class Redis { return await this.driver.del(this.ns(key)) } + async expire(key: string, seconds: number) { + return await this.driver.expire(this.ns(key), seconds) + } + + async zremrangebyscore(key: string, min: number, max: number) { + return await this.driver.zremrangebyscore(this.ns(key), min, max) + } + + async zcount(key: string, min: number, max: number) { + return await this.driver.zcount(this.ns(key), min, max) + } + + async zadd(key: string, score: number, member: number | string) { + return await this.driver.zadd(this.ns(key), score, member) + } + async destroy() { await this.driver.quit() } diff --git a/packages/bsky/src/services/indexing/index.ts b/packages/bsky/src/services/indexing/index.ts index e45cf25967f..de903532e85 100644 --- a/packages/bsky/src/services/indexing/index.ts +++ b/packages/bsky/src/services/indexing/index.ts @@ -28,6 +28,7 @@ import { subLogger } from '../../logger' import { retryHttp } from '../../util/retry' import { Labeler } from '../../labeler' import { BackgroundQueue } from '../../background' +import { NotificationServer } from '../../notifications' import { Actor } from '../../db/tables/actor' export class IndexingService { @@ -48,17 +49,22 @@ export class IndexingService { public idResolver: IdResolver, public labeler: Labeler, public backgroundQueue: BackgroundQueue, + public notifServer?: NotificationServer, ) { this.records = { - post: Post.makePlugin(this.db, backgroundQueue), - like: Like.makePlugin(this.db, backgroundQueue), - repost: Repost.makePlugin(this.db, backgroundQueue), - follow: Follow.makePlugin(this.db, backgroundQueue), - profile: Profile.makePlugin(this.db, backgroundQueue), - list: List.makePlugin(this.db, backgroundQueue), - listItem: ListItem.makePlugin(this.db, backgroundQueue), - block: Block.makePlugin(this.db, backgroundQueue), - feedGenerator: FeedGenerator.makePlugin(this.db, backgroundQueue), + post: Post.makePlugin(this.db, backgroundQueue, notifServer), + like: Like.makePlugin(this.db, backgroundQueue, notifServer), + repost: Repost.makePlugin(this.db, backgroundQueue, notifServer), + follow: Follow.makePlugin(this.db, backgroundQueue, notifServer), + profile: Profile.makePlugin(this.db, backgroundQueue, notifServer), + list: List.makePlugin(this.db, backgroundQueue, notifServer), + listItem: ListItem.makePlugin(this.db, backgroundQueue, notifServer), + block: Block.makePlugin(this.db, backgroundQueue, notifServer), + feedGenerator: FeedGenerator.makePlugin( + this.db, + backgroundQueue, + notifServer, + ), } } @@ -69,6 +75,7 @@ export class IndexingService { this.idResolver, this.labeler, this.backgroundQueue, + this.notifServer, ) } @@ -76,9 +83,10 @@ export class IndexingService { idResolver: IdResolver, labeler: Labeler, backgroundQueue: BackgroundQueue, + notifServer?: NotificationServer, ) { return (db: PrimaryDatabase) => - new IndexingService(db, idResolver, labeler, backgroundQueue) + new IndexingService(db, idResolver, labeler, backgroundQueue, notifServer) } async indexRecord( diff --git a/packages/bsky/src/services/indexing/plugins/block.ts b/packages/bsky/src/services/indexing/plugins/block.ts index 0686d1e6889..515166735e1 100644 --- a/packages/bsky/src/services/indexing/plugins/block.ts +++ b/packages/bsky/src/services/indexing/plugins/block.ts @@ -8,6 +8,7 @@ import RecordProcessor from '../processor' import { toSimplifiedISOSafe } from '../util' import { PrimaryDatabase } from '../../../db' import { BackgroundQueue } from '../../../background' +import { NotificationServer } from '../../../notifications' const lexId = lex.ids.AppBskyGraphBlock type IndexedBlock = Selectable @@ -74,8 +75,9 @@ export type PluginType = RecordProcessor export const makePlugin = ( db: PrimaryDatabase, backgroundQueue: BackgroundQueue, + notifServer?: NotificationServer, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, { + return new RecordProcessor(db, backgroundQueue, notifServer, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/feed-generator.ts b/packages/bsky/src/services/indexing/plugins/feed-generator.ts index 38da130a270..97b3baaed04 100644 --- a/packages/bsky/src/services/indexing/plugins/feed-generator.ts +++ b/packages/bsky/src/services/indexing/plugins/feed-generator.ts @@ -8,6 +8,7 @@ import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import { BackgroundQueue } from '../../../background' import RecordProcessor from '../processor' import { toSimplifiedISOSafe } from '../util' +import { NotificationServer } from '../../../notifications' const lexId = lex.ids.AppBskyFeedGenerator type IndexedFeedGenerator = Selectable @@ -73,8 +74,9 @@ export type PluginType = RecordProcessor< export const makePlugin = ( db: PrimaryDatabase, backgroundQueue: BackgroundQueue, + notifServer?: NotificationServer, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, { + return new RecordProcessor(db, backgroundQueue, notifServer, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/follow.ts b/packages/bsky/src/services/indexing/plugins/follow.ts index 647fd707296..29dcf88668b 100644 --- a/packages/bsky/src/services/indexing/plugins/follow.ts +++ b/packages/bsky/src/services/indexing/plugins/follow.ts @@ -9,6 +9,7 @@ import { toSimplifiedISOSafe } from '../util' import { PrimaryDatabase } from '../../../db' import { countAll, excluded } from '../../../db/util' import { BackgroundQueue } from '../../../background' +import { NotificationServer } from '../../../notifications' const lexId = lex.ids.AppBskyGraphFollow type IndexedFollow = Selectable @@ -121,8 +122,9 @@ export type PluginType = RecordProcessor export const makePlugin = ( db: PrimaryDatabase, backgroundQueue: BackgroundQueue, + notifServer?: NotificationServer, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, { + return new RecordProcessor(db, backgroundQueue, notifServer, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/like.ts b/packages/bsky/src/services/indexing/plugins/like.ts index 46466716a07..27a97a3c6a0 100644 --- a/packages/bsky/src/services/indexing/plugins/like.ts +++ b/packages/bsky/src/services/indexing/plugins/like.ts @@ -9,6 +9,7 @@ import { toSimplifiedISOSafe } from '../util' import { countAll, excluded } from '../../../db/util' import { PrimaryDatabase } from '../../../db' import { BackgroundQueue } from '../../../background' +import { NotificationServer } from '../../../notifications' const lexId = lex.ids.AppBskyFeedLike type IndexedLike = Selectable @@ -111,8 +112,9 @@ export type PluginType = RecordProcessor export const makePlugin = ( db: PrimaryDatabase, backgroundQueue: BackgroundQueue, + notifServer?: NotificationServer, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, { + return new RecordProcessor(db, backgroundQueue, notifServer, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/list-item.ts b/packages/bsky/src/services/indexing/plugins/list-item.ts index a396858412c..83ed29d9835 100644 --- a/packages/bsky/src/services/indexing/plugins/list-item.ts +++ b/packages/bsky/src/services/indexing/plugins/list-item.ts @@ -9,6 +9,7 @@ import { toSimplifiedISOSafe } from '../util' import { InvalidRequestError } from '@atproto/xrpc-server' import { PrimaryDatabase } from '../../../db' import { BackgroundQueue } from '../../../background' +import { NotificationServer } from '../../../notifications' const lexId = lex.ids.AppBskyGraphListitem type IndexedListItem = Selectable @@ -82,8 +83,9 @@ export type PluginType = RecordProcessor export const makePlugin = ( db: PrimaryDatabase, backgroundQueue: BackgroundQueue, + notifServer?: NotificationServer, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, { + return new RecordProcessor(db, backgroundQueue, notifServer, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/list.ts b/packages/bsky/src/services/indexing/plugins/list.ts index 38bdc38a9ce..6c2a46d003b 100644 --- a/packages/bsky/src/services/indexing/plugins/list.ts +++ b/packages/bsky/src/services/indexing/plugins/list.ts @@ -8,6 +8,7 @@ import RecordProcessor from '../processor' import { toSimplifiedISOSafe } from '../util' import { PrimaryDatabase } from '../../../db' import { BackgroundQueue } from '../../../background' +import { NotificationServer } from '../../../notifications' const lexId = lex.ids.AppBskyGraphList type IndexedList = Selectable @@ -70,8 +71,9 @@ export type PluginType = RecordProcessor export const makePlugin = ( db: PrimaryDatabase, backgroundQueue: BackgroundQueue, + notifServer?: NotificationServer, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, { + return new RecordProcessor(db, backgroundQueue, notifServer, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/post.ts b/packages/bsky/src/services/indexing/plugins/post.ts index 79a373e9174..7e2900e41e1 100644 --- a/packages/bsky/src/services/indexing/plugins/post.ts +++ b/packages/bsky/src/services/indexing/plugins/post.ts @@ -19,6 +19,7 @@ import { PrimaryDatabase } from '../../../db' import { countAll, excluded } from '../../../db/util' import { BackgroundQueue } from '../../../background' import { getAncestorsAndSelfQb, getDescendentsQb } from '../../util/post' +import { NotificationServer } from '../../../notifications' type Notif = Insertable type Post = Selectable @@ -356,8 +357,9 @@ export type PluginType = RecordProcessor export const makePlugin = ( db: PrimaryDatabase, backgroundQueue: BackgroundQueue, + notifServer?: NotificationServer, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, { + return new RecordProcessor(db, backgroundQueue, notifServer, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/profile.ts b/packages/bsky/src/services/indexing/plugins/profile.ts index 5024c93ab02..68dec0ccfa6 100644 --- a/packages/bsky/src/services/indexing/plugins/profile.ts +++ b/packages/bsky/src/services/indexing/plugins/profile.ts @@ -6,6 +6,7 @@ import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' import RecordProcessor from '../processor' import { PrimaryDatabase } from '../../../db' import { BackgroundQueue } from '../../../background' +import { NotificationServer } from '../../../notifications' const lexId = lex.ids.AppBskyActorProfile type IndexedProfile = DatabaseSchemaType['profile'] @@ -65,8 +66,9 @@ export type PluginType = RecordProcessor export const makePlugin = ( db: PrimaryDatabase, backgroundQueue: BackgroundQueue, + notifServer?: NotificationServer, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, { + return new RecordProcessor(db, backgroundQueue, notifServer, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/plugins/repost.ts b/packages/bsky/src/services/indexing/plugins/repost.ts index 63ef69424a3..7817f881e2f 100644 --- a/packages/bsky/src/services/indexing/plugins/repost.ts +++ b/packages/bsky/src/services/indexing/plugins/repost.ts @@ -9,6 +9,7 @@ import { toSimplifiedISOSafe } from '../util' import { PrimaryDatabase } from '../../../db' import { countAll, excluded } from '../../../db/util' import { BackgroundQueue } from '../../../background' +import { NotificationServer } from '../../../notifications' const lexId = lex.ids.AppBskyFeedRepost type IndexedRepost = Selectable @@ -136,8 +137,9 @@ export type PluginType = RecordProcessor export const makePlugin = ( db: PrimaryDatabase, backgroundQueue: BackgroundQueue, + notifServer?: NotificationServer, ): PluginType => { - return new RecordProcessor(db, backgroundQueue, { + return new RecordProcessor(db, backgroundQueue, notifServer, { lexId, insertFn, findDuplicate, diff --git a/packages/bsky/src/services/indexing/processor.ts b/packages/bsky/src/services/indexing/processor.ts index 3321ad5b634..5730f3ac97b 100644 --- a/packages/bsky/src/services/indexing/processor.ts +++ b/packages/bsky/src/services/indexing/processor.ts @@ -8,6 +8,8 @@ import { Notification } from '../../db/tables/notification' import { chunkArray } from '@atproto/common' import { PrimaryDatabase } from '../../db' import { BackgroundQueue } from '../../background' +import { NotificationServer } from '../../notifications' +import { dbLogger } from '../../logger' // @NOTE re: insertions and deletions. Due to how record updates are handled, // (insertFn) should have the same effect as (insertFn -> deleteFn -> insertFn). @@ -42,6 +44,7 @@ export class RecordProcessor { constructor( private appDb: PrimaryDatabase, private backgroundQueue: BackgroundQueue, + private notifServer: NotificationServer | undefined, private params: RecordProcessorParams, ) { this.db = appDb.db @@ -210,6 +213,7 @@ export class RecordProcessor { async handleNotifs(op: { deleted?: S; inserted?: S }) { let notifs: Notif[] = [] const runOnCommit: ((db: PrimaryDatabase) => Promise)[] = [] + const sendOnCommit: (() => Promise)[] = [] if (op.deleted) { const forDelete = this.params.notifsForDelete( op.deleted, @@ -233,6 +237,17 @@ export class RecordProcessor { runOnCommit.push(async (db) => { await db.db.insertInto('notification').values(chunk).execute() }) + if (this.notifServer) { + const notifServer = this.notifServer + sendOnCommit.push(async () => { + try { + const preparedNotifs = await notifServer.prepareNotifsToSend(chunk) + await notifServer.processNotifications(preparedNotifs) + } catch (error) { + dbLogger.error({ error }, 'error sending push notifications') + } + }) + } } if (runOnCommit.length) { // Need to ensure notif deletion always happens before creation, otherwise delete may clobber in a race. @@ -244,6 +259,16 @@ export class RecordProcessor { }) }) } + if (sendOnCommit.length) { + // Need to ensure notif deletion always happens before creation, otherwise delete may clobber in a race. + this.appDb.onCommit(() => { + this.backgroundQueue.add(async () => { + for (const fn of sendOnCommit) { + await fn() + } + }) + }) + } } aggregateOnCommit(indexed: S) { diff --git a/packages/bsky/tests/notification-server.test.ts b/packages/bsky/tests/notification-server.test.ts new file mode 100644 index 00000000000..55aa30029b4 --- /dev/null +++ b/packages/bsky/tests/notification-server.test.ts @@ -0,0 +1,146 @@ +import AtpAgent from '@atproto/api' +import { TestNetwork } from '@atproto/dev-env' +import { SeedClient } from './seeds/client' +import basicSeed from './seeds/basic' +import { NotificationServer } from '../src/notifications' +import { Database } from '../src' + +describe('notification server', () => { + let network: TestNetwork + let agent: AtpAgent + let sc: SeedClient + let notifServer: NotificationServer + + // account dids, for convenience + let alice: string + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_notification_server', + }) + agent = network.bsky.getClient() + const pdsAgent = network.pds.getClient() + sc = new SeedClient(pdsAgent) + await basicSeed(sc) + await network.processAll() + await network.bsky.processAll() + alice = sc.dids.alice + notifServer = network.bsky.ctx.notifServer + }) + + afterAll(async () => { + await network.close() + }) + + describe('registerPushNotification', () => { + it('registers push notification token and device.', async () => { + const res = await agent.api.app.bsky.notification.registerPush( + { + serviceDid: network.bsky.ctx.cfg.serverDid, + platform: 'ios', + token: '123', + appId: 'xyz.blueskyweb.app', + }, + { + encoding: 'application/json', + headers: await network.serviceHeaders(alice), + }, + ) + expect(res.success).toEqual(true) + }) + + it('allows reregistering push notification token.', async () => { + const res1 = await agent.api.app.bsky.notification.registerPush( + { + serviceDid: network.bsky.ctx.cfg.serverDid, + platform: 'web', + token: '234', + appId: 'xyz.blueskyweb.app', + }, + { + encoding: 'application/json', + headers: await network.serviceHeaders(alice), + }, + ) + const res2 = await agent.api.app.bsky.notification.registerPush( + { + serviceDid: network.bsky.ctx.cfg.serverDid, + platform: 'web', + token: '234', + appId: 'xyz.blueskyweb.app', + }, + { + encoding: 'application/json', + headers: await network.serviceHeaders(alice), + }, + ) + expect(res1.success).toEqual(true) + expect(res2.success).toEqual(true) + }) + + it('does not allows registering push notification at mismatching service.', async () => { + const tryRegister = agent.api.app.bsky.notification.registerPush( + { + serviceDid: 'did:web:notifservice.com', + platform: 'ios', + token: '123', + appId: 'xyz.blueskyweb.app', + }, + { + encoding: 'application/json', + headers: await network.serviceHeaders(alice), + }, + ) + await expect(tryRegister).rejects.toThrow('Invalid serviceDid.') + }) + }) + + describe('NotificationServer', () => { + it('gets user tokens from db', async () => { + const tokens = await notifServer.getTokensByDid([alice]) + expect(tokens[alice][0].token).toEqual('123') + }) + + it('gets notification display attributes: title and body', async () => { + const db = network.bsky.ctx.db.getPrimary() + const notif = await getLikeNotification(db, alice) + if (!notif) throw new Error('no notification found') + const attrs = await notifServer.getNotificationDisplayAttributes([notif]) + if (!attrs.length) + throw new Error('no notification display attributes found') + expect(attrs[0].title).toEqual('bobby liked your post') + }) + + it('prepares notification to be sent', async () => { + const db = network.bsky.ctx.db.getPrimary() + const notif = await getLikeNotification(db, alice) + if (!notif) throw new Error('no notification found') + const notifAsArray = [notif] + const prepared = await notifServer.prepareNotifsToSend(notifAsArray) + expect(prepared).toEqual([ + { + data: { + reason: notif.reason, + recordCid: notif.recordCid, + recordUri: notif.recordUri, + }, + message: 'again', + platform: 1, + title: 'bobby liked your post', + tokens: ['123'], + topic: 'xyz.blueskyweb.app', + }, + ]) + }) + }) + + async function getLikeNotification(db: Database, did: string) { + return await db.db + .selectFrom('notification') + .selectAll() + .where('did', '=', did) + .where('reason', '=', 'like') + .orderBy('sortAt') + .executeTakeFirst() + } +}) diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index d0a85d1f3b3..336e16ec6a6 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -97,6 +97,7 @@ export class TestBsky { indexerSubLockId: uniqueLockId(), indexerPort: await getPort(), ingesterPartitionCount: 1, + pushNotificationEndpoint: 'https://push.bsky.app/api/push', }) assert(indexerCfg.redisHost) const indexerRedis = new bsky.Redis({ diff --git a/packages/identity/src/did/atproto-data.ts b/packages/identity/src/did/atproto-data.ts index d8cd3d06f91..ae958a784cc 100644 --- a/packages/identity/src/did/atproto-data.ts +++ b/packages/identity/src/did/atproto-data.ts @@ -42,41 +42,24 @@ export const getHandle = (doc: DidDocument): string | undefined => { } export const getPds = (doc: DidDocument): string | undefined => { - let services = doc.service - if (!services) return undefined - if (typeof services !== 'object') return undefined - if (!Array.isArray(services)) { - services = [services] - } - const found = services.find((service) => service.id === '#atproto_pds') - if (!found) return undefined - if (found.type !== 'AtprotoPersonalDataServer') { - return undefined - } - if (typeof found.serviceEndpoint !== 'string') { - return undefined - } - validateUrl(found.serviceEndpoint) - return found.serviceEndpoint + return getServiceEndpoint(doc, { + id: '#atproto_pds', + type: 'AtprotoPersonalDataServer', + }) } export const getFeedGen = (doc: DidDocument): string | undefined => { - let services = doc.service - if (!services) return undefined - if (typeof services !== 'object') return undefined - if (!Array.isArray(services)) { - services = [services] - } - const found = services.find((service) => service.id === '#bsky_fg') - if (!found) return undefined - if (found.type !== 'BskyFeedGenerator') { - return undefined - } - if (typeof found.serviceEndpoint !== 'string') { - return undefined - } - validateUrl(found.serviceEndpoint) - return found.serviceEndpoint + return getServiceEndpoint(doc, { + id: '#bsky_fg', + type: 'BskyFeedGenerator', + }) +} + +export const getNotif = (doc: DidDocument): string | undefined => { + return getServiceEndpoint(doc, { + id: '#bsky_notif', + type: 'BskyNotificationService', + }) } export const parseToAtprotoDocument = ( @@ -118,3 +101,25 @@ const validateUrl = (url: string) => { throw new Error('Invalid pds hostname') } } + +const getServiceEndpoint = ( + doc: DidDocument, + opts: { id: string; type: string }, +) => { + let services = doc.service + if (!services) return undefined + if (typeof services !== 'object') return undefined + if (!Array.isArray(services)) { + services = [services] + } + const found = services.find((service) => service.id === opts.id) + if (!found) return undefined + if (found.type !== opts.type) { + return undefined + } + if (typeof found.serviceEndpoint !== 'string') { + return undefined + } + validateUrl(found.serviceEndpoint) + return found.serviceEndpoint +} diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts index 50652c6c65c..3ef12fa48c6 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getFeed.ts @@ -6,11 +6,7 @@ import { serverTimingHeader, } from '@atproto/xrpc-server' import { ResponseType, XRPCError } from '@atproto/xrpc' -import { - DidDocument, - PoorlyFormattedDidDocumentError, - getFeedGen, -} from '@atproto/identity' +import { getFeedGen } from '@atproto/identity' import { AtpAgent, AppBskyFeedGetFeedSkeleton } from '@atproto/api' import { SkeletonFeedPost } from '../../../../../lexicon/types/app/bsky/feed/defs' import { QueryParams as GetFeedParams } from '../../../../../lexicon/types/app/bsky/feed/getFeed' @@ -19,6 +15,7 @@ import { Server } from '../../../../../lexicon' import AppContext from '../../../../../context' import { FeedRow } from '../../../../services/feed' import { AlgoResponse } from '../../../../../feed-gen/types' +import { getDidDoc } from '../util/resolver' export default function (server: Server, ctx: AppContext) { const isProxyableFeed = (feed: string): boolean => { @@ -104,20 +101,8 @@ async function skeletonFromFeedGen( } const feedDid = found.feedDid - let resolved: DidDocument | null - try { - resolved = await ctx.idResolver.did.resolve(feedDid) - } catch (err) { - if (err instanceof PoorlyFormattedDidDocumentError) { - throw new InvalidRequestError(`invalid did document: ${feedDid}`) - } - throw err - } - if (!resolved) { - throw new InvalidRequestError(`could not resolve did document: ${feedDid}`) - } - - const fgEndpoint = getFeedGen(resolved) + const doc = await getDidDoc(ctx, feedDid) + const fgEndpoint = getFeedGen(doc) if (!fgEndpoint) { throw new InvalidRequestError( `invalid feed generator service details in did document: ${feedDid}`, diff --git a/packages/pds/src/app-view/api/app/bsky/index.ts b/packages/pds/src/app-view/api/app/bsky/index.ts index a4055240844..d9358460c1e 100644 --- a/packages/pds/src/app-view/api/app/bsky/index.ts +++ b/packages/pds/src/app-view/api/app/bsky/index.ts @@ -31,6 +31,7 @@ import getSuggestions from './actor/getSuggestions' import listNotifications from './notification/listNotifications' import getUnreadCount from './notification/getUnreadCount' import updateSeen from './notification/updateSeen' +import registerPush from './notification/registerPush' import unspecced from './unspecced' export default function (server: Server, ctx: AppContext) { @@ -65,5 +66,6 @@ export default function (server: Server, ctx: AppContext) { listNotifications(server, ctx) getUnreadCount(server, ctx) updateSeen(server, ctx) + registerPush(server, ctx) unspecced(server, ctx) } diff --git a/packages/pds/src/app-view/api/app/bsky/notification/registerPush.ts b/packages/pds/src/app-view/api/app/bsky/notification/registerPush.ts new file mode 100644 index 00000000000..bb2d329e8e7 --- /dev/null +++ b/packages/pds/src/app-view/api/app/bsky/notification/registerPush.ts @@ -0,0 +1,47 @@ +import { Server } from '../../../../../lexicon' +import AppContext from '../../../../../context' +import { getNotif } from '@atproto/identity' +import { InvalidRequestError } from '@atproto/xrpc-server' +import { AtpAgent } from '@atproto/api' +import { getDidDoc } from '../util/resolver' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.notification.registerPush({ + auth: ctx.accessVerifier, + handler: async ({ auth, input }) => { + const { serviceDid } = input.body + const { + credentials: { did }, + } = auth + + const authHeaders = await ctx.serviceAuthHeaders(did, serviceDid) + + if (ctx.canProxyWrite() && ctx.cfg.bskyAppViewDid === serviceDid) { + const { appviewAgent } = ctx + await appviewAgent.api.app.bsky.notification.registerPush(input.body, { + ...authHeaders, + encoding: 'application/json', + }) + return + } + + const notifEndpoint = await getEndpoint(ctx, serviceDid) + const agent = new AtpAgent({ service: notifEndpoint }) + await agent.api.app.bsky.notification.registerPush(input.body, { + ...authHeaders, + encoding: 'application/json', + }) + }, + }) +} + +const getEndpoint = async (ctx: AppContext, serviceDid: string) => { + const doc = await getDidDoc(ctx, serviceDid) + const notifEndpoint = getNotif(doc) + if (!notifEndpoint) { + throw new InvalidRequestError( + `invalid notification service details in did document: ${serviceDid}`, + ) + } + return notifEndpoint +} diff --git a/packages/pds/src/app-view/api/app/bsky/util/resolver.ts b/packages/pds/src/app-view/api/app/bsky/util/resolver.ts new file mode 100644 index 00000000000..9be31998439 --- /dev/null +++ b/packages/pds/src/app-view/api/app/bsky/util/resolver.ts @@ -0,0 +1,20 @@ +import { DidDocument, PoorlyFormattedDidDocumentError } from '@atproto/identity' +import AppContext from '../../../../../context' +import { InvalidRequestError } from '@atproto/xrpc-server' + +// provides http-friendly errors during did resolution +export const getDidDoc = async (ctx: AppContext, did: string) => { + let resolved: DidDocument | null + try { + resolved = await ctx.idResolver.did.resolve(did) + } catch (err) { + if (err instanceof PoorlyFormattedDidDocumentError) { + throw new InvalidRequestError(`invalid did document: ${did}`) + } + throw err + } + if (!resolved) { + throw new InvalidRequestError(`could not resolve did document: ${did}`) + } + return resolved +} diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 8b33f2c2c21..7143c1833a4 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -102,6 +102,7 @@ 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 AppBskyUnspeccedApplyLabels from './types/app/bsky/unspecced/applyLabels' import * as AppBskyUnspeccedGetPopular from './types/app/bsky/unspecced/getPopular' @@ -1324,6 +1325,17 @@ export class NotificationNS { 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, diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index ab7e2febe1b..b5c82ce2f73 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -3575,6 +3575,8 @@ export const schemaDict = { }, 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: { @@ -6367,6 +6369,39 @@ export const schemaDict = { }, }, }, + 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', @@ -6746,6 +6781,7 @@ export const ids = { 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', AppBskyUnspeccedApplyLabels: 'app.bsky.unspecced.applyLabels', diff --git a/packages/pds/src/lexicon/types/app/bsky/notification/registerPush.ts b/packages/pds/src/lexicon/types/app/bsky/notification/registerPush.ts new file mode 100644 index 00000000000..9923aeb058e --- /dev/null +++ b/packages/pds/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/pds/src/lexicon/types/com/atproto/sync/subscribeRepos.ts b/packages/pds/src/lexicon/types/com/atproto/sync/subscribeRepos.ts index 9b5326ed347..7fafb749398 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/subscribeRepos.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/subscribeRepos.ts @@ -136,6 +136,7 @@ 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 diff --git a/packages/pds/tests/proxied/notif.test.ts b/packages/pds/tests/proxied/notif.test.ts new file mode 100644 index 00000000000..106620d4bcd --- /dev/null +++ b/packages/pds/tests/proxied/notif.test.ts @@ -0,0 +1,91 @@ +import { once } from 'events' +import http from 'http' +import { AddressInfo } from 'net' +import express from 'express' +import AtpAgent from '@atproto/api' +import { TestNetworkNoAppView } from '@atproto/dev-env' +import { verifyJwt } from '@atproto/xrpc-server' +import { SeedClient } from '../seeds/client' +import usersSeed from '../seeds/users' +import { createServer } from '../../src/lexicon' + +describe('notif service proxy', () => { + let network: TestNetworkNoAppView + let notifServer: http.Server + let notifDid: string + let agent: AtpAgent + let sc: SeedClient + const spy: { current: unknown } = { current: null } + + beforeAll(async () => { + network = await TestNetworkNoAppView.create({ + dbPostgresSchema: 'proxy_notifs', + }) + network.pds.server.app.get + const plc = network.plc.getClient() + agent = network.pds.getClient() + sc = new SeedClient(agent) + await usersSeed(sc) + await network.processAll() + // piggybacking existing plc did, turn it into a notif service + notifServer = await createMockNotifService(spy) + notifDid = sc.dids.dan + await plc.updateData(notifDid, network.pds.ctx.plcRotationKey, (x) => { + const addr = notifServer.address() as AddressInfo + x.services['bsky_notif'] = { + type: 'BskyNotificationService', + endpoint: `http://localhost:${addr.port}`, + } + return x + }) + }) + + afterAll(async () => { + await network.close() + notifServer.close() + await once(notifServer, 'close') + }) + + it('proxies to notif service.', async () => { + await agent.api.app.bsky.notification.registerPush( + { + serviceDid: notifDid, + token: 'tok1', + platform: 'web', + appId: 'app1', + }, + { + headers: sc.getHeaders(sc.dids.bob), + encoding: 'application/json', + }, + ) + expect(spy.current?.['input']).toEqual({ + serviceDid: notifDid, + token: 'tok1', + platform: 'web', + appId: 'app1', + }) + + const auth = await verifyJwt( + spy.current?.['jwt'] as string, + notifDid, + async () => network.pds.ctx.repoSigningKey.did(), + ) + expect(auth).toEqual(sc.dids.bob) + }) +}) + +async function createMockNotifService(ref: { current: unknown }) { + const app = express() + const svc = createServer() + svc.app.bsky.notification.registerPush(({ input, req }) => { + ref.current = { + input: input.body, + jwt: req.headers.authorization?.replace('Bearer ', ''), + } + }) + app.use(svc.xrpc.router) + const server = app.listen() + await once(server, 'listening') + return server +} diff --git a/yarn.lock b/yarn.lock index ee4cebbfc37..46f614845e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3374,6 +3374,11 @@ cborg "^1.6.0" multiformats "^9.5.4" +"@isaacs/ttlcache@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz#21fb23db34e9b6220c6ba023a0118a2dd3461ea2" + integrity sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" From 1dd0c46593932047fd3d78bdd47ca51a771bc93f Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 23 Aug 2023 16:18:04 -0700 Subject: [PATCH 185/237] @atproto/api@0.6.6 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index cfb25c636f9..b4797a0bc02 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.5", + "version": "0.6.6", "main": "src/index.ts", "scripts": { "codegen": "yarn docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From 82ef0ad6d73dcadae957f6b81e2db25554b29d29 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Thu, 24 Aug 2023 17:25:16 +0200 Subject: [PATCH 186/237] :sparkles: Add self labels to getRecord and getRepo on com.atproto namespace (#1488) * :sparkles: Add self labels to getRecord and getRepo * :white_check_mark: Update snapshot to include self labelling --- .../bsky/src/services/moderation/views.ts | 8 ++++- packages/pds/src/services/moderation/views.ts | 9 +++++- .../__snapshots__/get-record.test.ts.snap | 30 +++++++++++++++++-- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/packages/bsky/src/services/moderation/views.ts b/packages/bsky/src/services/moderation/views.ts index 6c9effe8684..f75005143c0 100644 --- a/packages/bsky/src/services/moderation/views.ts +++ b/packages/bsky/src/services/moderation/views.ts @@ -21,6 +21,7 @@ import { import { OutputSchema as ReportOutput } from '../../lexicon/types/com/atproto/moderation/createReport' import { Label } from '../../lexicon/types/com/atproto/label/defs' import { ModerationReportRowWithHandle } from '.' +import { getSelfLabels } from '../label' export class ModerationViews { constructor(private db: Database) {} @@ -227,6 +228,11 @@ export class ModerationViews { this.blob(findBlobRefs(record.value)), this.labels(record.uri), ]) + const selfLabels = getSelfLabels({ + uri: result.uri, + cid: result.cid, + record: jsonStringToLex(result.json) as Record, + }) return { ...record, blobs, @@ -235,7 +241,7 @@ export class ModerationViews { reports, actions, }, - labels, + labels: [...labels, ...selfLabels], } } diff --git a/packages/pds/src/services/moderation/views.ts b/packages/pds/src/services/moderation/views.ts index 2a0a80297d3..8f0400cfab4 100644 --- a/packages/pds/src/services/moderation/views.ts +++ b/packages/pds/src/services/moderation/views.ts @@ -22,6 +22,8 @@ import { ModerationAction } from '../../db/tables/moderation' import { AccountService } from '../account' import { RecordService } from '../record' import { ModerationReportRowWithHandle } from '.' +import { getSelfLabels } from '../../app-view/services/label' +import { jsonStringToLex } from '@atproto/lexicon' export class ModerationViews { constructor(private db: Database, private messageDispatcher: MessageQueue) {} @@ -275,6 +277,11 @@ export class ModerationViews { this.blob(record.blobCids), this.labels(record.uri), ]) + const selfLabels = getSelfLabels({ + uri: result.uri, + cid: result.cid, + record: result.value as Record, + }) return { ...record, blobs, @@ -283,7 +290,7 @@ export class ModerationViews { reports, actions, }, - labels, + labels: [...labels, ...selfLabels], } } diff --git a/packages/pds/tests/views/admin/__snapshots__/get-record.test.ts.snap b/packages/pds/tests/views/admin/__snapshots__/get-record.test.ts.snap index c9eda38e356..3a816975459 100644 --- a/packages/pds/tests/views/admin/__snapshots__/get-record.test.ts.snap +++ b/packages/pds/tests/views/admin/__snapshots__/get-record.test.ts.snap @@ -6,7 +6,16 @@ Object { "blobs": Array [], "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label", + }, + ], "moderation": Object { "actions": Array [ Object { @@ -134,7 +143,16 @@ Object { "blobs": Array [], "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", - "labels": Array [], + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label", + }, + ], "moderation": Object { "actions": Array [ Object { @@ -279,6 +297,14 @@ Object { "uri": "record(0)", "val": "puppies", }, + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label", + }, ], "moderation": Object { "actions": Array [ From d614678c49f9a8f65c9af1f9a93d569f6b8dc541 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Thu, 24 Aug 2023 17:27:33 +0200 Subject: [PATCH 187/237] Revert labels on takedown reversal (#1496) * :sparkles: Revert labels on takedown reversal * :white_check_mark: Add tests to cover label reversal * :white_check_mark: Add label reversal test to bsky package * :zap: Move xrpc call out of db transaction --- .../db/periodic-moderation-action-reversal.ts | 58 ++++++++++++++----- .../bsky/src/services/moderation/index.ts | 30 +++++----- packages/bsky/tests/moderation.test.ts | 48 +++++++++++++++ .../db/periodic-moderation-action-reversal.ts | 32 +++++++++- packages/pds/src/services/moderation/index.ts | 6 +- packages/pds/tests/moderation.test.ts | 8 +++ 6 files changed, 143 insertions(+), 39 deletions(-) diff --git a/packages/bsky/src/db/periodic-moderation-action-reversal.ts b/packages/bsky/src/db/periodic-moderation-action-reversal.ts index be8e4044477..d15cef91afb 100644 --- a/packages/bsky/src/db/periodic-moderation-action-reversal.ts +++ b/packages/bsky/src/db/periodic-moderation-action-reversal.ts @@ -4,6 +4,8 @@ import { dbLogger } from '../logger' import AppContext from '../context' import AtpAgent from '@atproto/api' import { buildBasicAuth } from '../auth' +import { LabelService } from '../services/label' +import { ModerationActionRow } from '../services/moderation' export const MODERATION_ACTION_REVERSAL_ID = 1011 @@ -26,22 +28,48 @@ export class PeriodicModerationActionReversal { } } - async revertAction({ id, createdBy }: { id: number; createdBy: string }) { - return this.appContext.db.getPrimary().transaction(async (dbTxn) => { + // invert label creation & negations + async reverseLabels(labelTxn: LabelService, actionRow: ModerationActionRow) { + let uri: string + let cid: string | null = null + + if (actionRow.subjectUri && actionRow.subjectCid) { + uri = actionRow.subjectUri + cid = actionRow.subjectCid + } else { + uri = actionRow.subjectDid + } + + await labelTxn.formatAndCreate(this.appContext.cfg.labelerDid, uri, cid, { + create: actionRow.negateLabelVals + ? actionRow.negateLabelVals.split(' ') + : undefined, + negate: actionRow.createLabelVals + ? actionRow.createLabelVals.split(' ') + : undefined, + }) + } + + async revertAction(actionRow: ModerationActionRow) { + const reverseAction = { + id: actionRow.id, + createdBy: actionRow.createdBy, + createdAt: new Date(), + reason: `[SCHEDULED_REVERSAL] Reverting action as originally scheduled`, + } + + if (this.pushAgent) { + await this.pushAgent.com.atproto.admin.reverseModerationAction( + reverseAction, + ) + return + } + + await this.appContext.db.getPrimary().transaction(async (dbTxn) => { const moderationTxn = this.appContext.services.moderation(dbTxn) - const reverseAction = { - id, - createdBy, - createdAt: new Date(), - reason: `[SCHEDULED_REVERSAL] Reverting action as originally scheduled`, - } - if (this.pushAgent) { - await this.pushAgent.com.atproto.admin.reverseModerationAction( - reverseAction, - ) - } else { - await moderationTxn.revertAction(reverseAction) - } + await moderationTxn.revertAction(reverseAction) + const labelTxn = this.appContext.services.label(dbTxn) + await this.reverseLabels(labelTxn, actionRow) }) } diff --git a/packages/bsky/src/services/moderation/index.ts b/packages/bsky/src/services/moderation/index.ts index 3463e86605a..5d935046867 100644 --- a/packages/bsky/src/services/moderation/index.ts +++ b/packages/bsky/src/services/moderation/index.ts @@ -338,15 +338,13 @@ export class ModerationService { return actionResult } - async getActionsDueForReversal(): Promise< - Array<{ id: number; createdBy: string }> - > { + async getActionsDueForReversal(): Promise { const actionsDueForReversal = await this.db.db .selectFrom('moderation_action') .where('durationInHours', 'is not', null) .where('expiresAt', '<', new Date().toISOString()) .where('reversedAt', 'is', null) - .select(['id', 'createdBy']) + .selectAll() .execute() return actionsDueForReversal @@ -354,15 +352,10 @@ export class ModerationService { async revertAction({ id, - createdAt, createdBy, + createdAt, reason, - }: { - id: number - createdAt: Date - createdBy: string - reason: string - }) { + }: ReversibleModerationAction) { this.db.assertTransaction() const result = await this.logReverseAction({ id, @@ -394,12 +387,9 @@ export class ModerationService { return result } - async logReverseAction(info: { - id: number - reason: string - createdBy: string - createdAt?: Date - }): Promise { + async logReverseAction( + info: ReversibleModerationAction, + ): Promise { const { id, createdBy, reason, createdAt = new Date() } = info const result = await this.db.db @@ -574,6 +564,12 @@ export class ModerationService { } export type ModerationActionRow = Selectable +export type ReversibleModerationAction = Pick< + ModerationActionRow, + 'id' | 'createdBy' | 'reason' +> & { + createdAt?: Date +} export type ModerationReportRow = Selectable export type ModerationReportRowWithHandle = ModerationReportRow & { diff --git a/packages/bsky/tests/moderation.test.ts b/packages/bsky/tests/moderation.test.ts index 8b370ab4848..51561e191fa 100644 --- a/packages/bsky/tests/moderation.test.ts +++ b/packages/bsky/tests/moderation.test.ts @@ -15,6 +15,7 @@ import { REASONOTHER, REASONSPAM, } from '../src/lexicon/types/com/atproto/moderation/defs' +import { PeriodicModerationActionReversal } from '../src' describe('moderation', () => { let network: TestNetwork @@ -1003,6 +1004,53 @@ describe('moderation', () => { 'Must be a full moderator to perform an account takedown', ) }) + it('automatically reverses actions marked with duration', async () => { + const { data: action } = + await agent.api.com.atproto.admin.takeModerationAction( + { + action: TAKEDOWN, + createdBy: 'did:example:moderator', + reason: 'Y', + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.bob, + }, + createLabelVals: ['takendown'], + // Use negative value to set the expiry time in the past so that the action is automatically reversed + // right away without having to wait n number of hours for a successful assertion + durationInHours: -1, + }, + { + encoding: 'application/json', + headers: network.bsky.adminAuthHeaders('moderator'), + }, + ) + + const labelsAfterTakedown = await getRepoLabels(sc.dids.bob) + expect(labelsAfterTakedown).toContain('takendown') + // In the actual app, this will be instantiated and run on server startup + const periodicReversal = new PeriodicModerationActionReversal( + network.bsky.ctx, + ) + await periodicReversal.findAndRevertDueActions() + + const { data: reversedAction } = + await agent.api.com.atproto.admin.getModerationAction( + { id: action.id }, + { headers: network.bsky.adminAuthHeaders('moderator') }, + ) + + // 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(reversedAction.reversal).toMatchObject({ + createdBy: action.createdBy, + reason: '[SCHEDULED_REVERSAL] Reverting action as originally scheduled', + }) + + // Verify that labels are also reversed when takedown action is reversed + const labelsAfterReversal = await getRepoLabels(sc.dids.bob) + expect(labelsAfterReversal).not.toContain('takendown') + }) async function actionWithLabels( opts: Partial & { diff --git a/packages/pds/src/db/periodic-moderation-action-reversal.ts b/packages/pds/src/db/periodic-moderation-action-reversal.ts index 808fba5f369..72730e61565 100644 --- a/packages/pds/src/db/periodic-moderation-action-reversal.ts +++ b/packages/pds/src/db/periodic-moderation-action-reversal.ts @@ -3,6 +3,8 @@ import { wait } from '@atproto/common' import { Leader } from './leader' import { dbLogger } from '../logger' import AppContext from '../context' +import { ModerationActionRow } from '../services/moderation' +import { LabelService } from '../app-view/services/label' export const MODERATION_ACTION_REVERSAL_ID = 1011 @@ -12,15 +14,39 @@ export class PeriodicModerationActionReversal { constructor(private appContext: AppContext) {} - async revertAction({ id, createdBy }: { id: number; createdBy: string }) { + // invert label creation & negations + async reverseLabels(labelTxn: LabelService, actionRow: ModerationActionRow) { + let uri: string + let cid: string | null = null + + if (actionRow.subjectUri && actionRow.subjectCid) { + uri = actionRow.subjectUri + cid = actionRow.subjectCid + } else { + uri = actionRow.subjectDid + } + + await labelTxn.formatAndCreate(this.appContext.cfg.labelerDid, uri, cid, { + create: actionRow.negateLabelVals + ? actionRow.negateLabelVals.split(' ') + : undefined, + negate: actionRow.createLabelVals + ? actionRow.createLabelVals.split(' ') + : undefined, + }) + } + + async revertAction(actionRow: ModerationActionRow) { return this.appContext.db.transaction(async (dbTxn) => { const moderationTxn = this.appContext.services.moderation(dbTxn) + const labelTxn = this.appContext.services.appView.label(dbTxn) await moderationTxn.revertAction({ - id, - createdBy, + id: actionRow.id, + createdBy: actionRow.createdBy, createdAt: new Date(), reason: `[SCHEDULED_REVERSAL] Reverting action as originally scheduled`, }) + await this.reverseLabels(labelTxn, actionRow) }) } diff --git a/packages/pds/src/services/moderation/index.ts b/packages/pds/src/services/moderation/index.ts index 02433ea4f35..92ffdcc9d33 100644 --- a/packages/pds/src/services/moderation/index.ts +++ b/packages/pds/src/services/moderation/index.ts @@ -375,16 +375,14 @@ export class ModerationService { return actionResult } - async getActionsDueForReversal(): Promise< - Array<{ id: number; createdBy: string }> - > { + async getActionsDueForReversal(): Promise> { const actionsDueForReversal = await this.db.db .selectFrom('moderation_action') // Get entries that have an durationInHours that has passed and have not been reversed .where('durationInHours', 'is not', null) .where('expiresAt', '<', new Date().toISOString()) .where('reversedAt', 'is', null) - .select(['id', 'createdBy']) + .selectAll() .execute() return actionsDueForReversal diff --git a/packages/pds/tests/moderation.test.ts b/packages/pds/tests/moderation.test.ts index fd3731a8668..f47eb17a81e 100644 --- a/packages/pds/tests/moderation.test.ts +++ b/packages/pds/tests/moderation.test.ts @@ -1017,6 +1017,7 @@ describe('moderation', () => { $type: 'com.atproto.admin.defs#repoRef', did: sc.dids.bob, }, + createLabelVals: ['takendown'], // Use negative value to set the expiry time in the past so that the action is automatically reversed // right away without having to wait n number of hours for a successful assertion durationInHours: -1, @@ -1026,6 +1027,9 @@ describe('moderation', () => { headers: { authorization: moderatorAuth() }, }, ) + + const labelsAfterTakedown = await getRepoLabels(sc.dids.bob) + expect(labelsAfterTakedown).toContain('takendown') // In the actual app, this will be instantiated and run on server startup const periodicReversal = new PeriodicModerationActionReversal(server.ctx) await periodicReversal.findAndRevertDueActions() @@ -1042,6 +1046,10 @@ describe('moderation', () => { createdBy: action.createdBy, reason: '[SCHEDULED_REVERSAL] Reverting action as originally scheduled', }) + + // Verify that labels are also reversed when takedown action is reversed + const labelsAfterReversal = await getRepoLabels(sc.dids.bob) + expect(labelsAfterReversal).not.toContain('takendown') }) it('does not allow non-full moderators to takedown.', async () => { From 0a2d1f5e1e25ca4a18a65d736b886abd9dfe14a8 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 24 Aug 2023 12:37:05 -0500 Subject: [PATCH 188/237] Partial reprocessing of repos (#1508) * disable labels on reprocessing * wait for reprocess * avoid creating notifications on repo reindexing * partial reindexes * tidy * bail out case for push notifs * return -> continue * dont build branch --------- Co-authored-by: Devin Ivy --- packages/bsky/src/indexer/server.ts | 2 +- packages/bsky/src/indexer/subscription.ts | 6 +- packages/bsky/src/notifications.ts | 8 ++ packages/bsky/src/services/indexing/index.ts | 112 +++++++++++++++--- .../src/services/indexing/plugins/follow.ts | 2 +- .../src/services/indexing/plugins/like.ts | 2 +- .../src/services/indexing/plugins/post.ts | 12 +- .../src/services/indexing/plugins/repost.ts | 2 +- .../bsky/src/services/indexing/processor.ts | 24 +++- 9 files changed, 134 insertions(+), 36 deletions(-) diff --git a/packages/bsky/src/indexer/server.ts b/packages/bsky/src/indexer/server.ts index 1dfdd1a1ddb..dfafb741eb4 100644 --- a/packages/bsky/src/indexer/server.ts +++ b/packages/bsky/src/indexer/server.ts @@ -21,7 +21,7 @@ export const createServer = ( } catch (err) { return res.status(500).send('could not calculate partition') } - sub.requestReprocess(req.params.did) + await sub.requestReprocess(req.params.did) res.sendStatus(200) }) return app diff --git a/packages/bsky/src/indexer/subscription.ts b/packages/bsky/src/indexer/subscription.ts index 76262f8a82e..dc7fdd4ff80 100644 --- a/packages/bsky/src/indexer/subscription.ts +++ b/packages/bsky/src/indexer/subscription.ts @@ -106,10 +106,10 @@ export class IndexerSubscription { } } - requestReprocess(did: string) { - this.repoQueue.add(did, async () => { + async requestReprocess(did: string) { + await this.repoQueue.add(did, async () => { try { - await this.indexingSvc.indexRepo(did) + await this.indexingSvc.indexRepo(did, undefined) } catch (err) { log.error({ did }, 'failed to reprocess repo') } diff --git a/packages/bsky/src/notifications.ts b/packages/bsky/src/notifications.ts index cdcb35f7684..854ba64e2d1 100644 --- a/packages/bsky/src/notifications.ts +++ b/packages/bsky/src/notifications.ts @@ -62,6 +62,9 @@ export class NotificationServer { ) for (const notifView of notificationViews) { + if (!isRecent(notifView.notif.sortAt, 10 * MINUTE)) { + continue // if the notif is from > 10 minutes ago, don't send push notif + } const { did: userDid } = notifView.notif const userTokens = tokensByDid[userDid] ?? [] for (const t of userTokens) { @@ -291,6 +294,11 @@ export class NotificationServer { } } +const isRecent = (isoTime: string, timeDiff: number): boolean => { + const diff = Date.now() - new Date(isoTime).getTime() + return diff < timeDiff +} + const unique = (items: string[]) => [...new Set(items)] class RateLimiter { diff --git a/packages/bsky/src/services/indexing/index.ts b/packages/bsky/src/services/indexing/index.ts index de903532e85..fb5e4ec167e 100644 --- a/packages/bsky/src/services/indexing/index.ts +++ b/packages/bsky/src/services/indexing/index.ts @@ -11,7 +11,7 @@ import { } from '@atproto/repo' import { AtUri } from '@atproto/uri' import { IdResolver, getPds } from '@atproto/identity' -import { DAY, HOUR, chunkArray } from '@atproto/common' +import { DAY, HOUR } from '@atproto/common' import { ValidationError } from '@atproto/lexicon' import { PrimaryDatabase } from '../../db' import * as Post from './plugins/post' @@ -95,6 +95,7 @@ export class IndexingService { obj: unknown, action: WriteOpAction.Create | WriteOpAction.Update, timestamp: string, + opts?: { disableNotifs?: boolean; disableLabels?: boolean }, ) { this.db.assertNotTransaction() await this.db.transaction(async (txn) => { @@ -102,12 +103,14 @@ export class IndexingService { const indexer = indexingTx.findIndexerForCollection(uri.collection) if (!indexer) return if (action === WriteOpAction.Create) { - await indexer.insertRecord(uri, cid, obj, timestamp) + await indexer.insertRecord(uri, cid, obj, timestamp, opts) } else { await indexer.updateRecord(uri, cid, obj, timestamp) } }) - this.labeler.processRecord(uri, obj) + if (!opts?.disableLabels) { + this.labeler.processRecord(uri, obj) + } } async deleteRecord(uri: AtUri, cascading = false) { @@ -186,19 +189,25 @@ export class IndexingService { signingKey, ) - // Wipe index for actor, prep for reindexing - await this.unindexActor(did) - - // Iterate over all records and index them in batches - const contentList = [...walkContentsWithCids(checkout.contents)] - const chunks = chunkArray(contentList, 100) + const currRecords = await this.getCurrentRecords(did) + const checkoutRecords = formatCheckout(did, checkout.contents) + const diff = findDiffFromCheckout(currRecords, checkoutRecords) - for (const chunk of chunks) { - const processChunk = chunk.map(async (item) => { - const { cid, collection, rkey, record } = item - const uri = AtUri.make(did, collection, rkey) + await Promise.all( + diff.map(async (op) => { + const { uri, cid } = op try { - await this.indexRecord(uri, cid, record, WriteOpAction.Create, now) + if (op.op === 'delete') { + await this.deleteRecord(uri) + } else { + await this.indexRecord( + uri, + cid, + op.value, + op.op === 'create' ? WriteOpAction.Create : WriteOpAction.Update, + now, + ) + } } catch (err) { if (err instanceof ValidationError) { subLogger.warn( @@ -212,9 +221,23 @@ export class IndexingService { ) } } - }) - await Promise.all(processChunk) - } + }), + ) + } + + async getCurrentRecords(did: string) { + const res = await this.db.db + .selectFrom('record') + .where('did', '=', did) + .select(['uri', 'cid']) + .execute() + return res.reduce((acc, cur) => { + acc[cur.uri] = { + uri: new AtUri(cur.uri), + cid: CID.parse(cur.cid), + } + return acc + }, {} as Record) } async setCommitLastSeen( @@ -352,13 +375,64 @@ export class IndexingService { } } -function* walkContentsWithCids(contents: RepoContentsWithCids) { +type UriAndCid = { + uri: AtUri + cid: CID +} + +type RecordDescript = UriAndCid & { + value: unknown +} + +type IndexOp = + | ({ + op: 'create' | 'update' + } & RecordDescript) + | ({ op: 'delete' } & UriAndCid) + +const findDiffFromCheckout = ( + curr: Record, + checkout: Record, +): IndexOp[] => { + const ops: IndexOp[] = [] + for (const uri of Object.keys(checkout)) { + const record = checkout[uri] + if (!curr[uri]) { + ops.push({ op: 'create', ...record }) + } else { + if (curr[uri].cid.equals(record.cid)) { + // no-op + continue + } + ops.push({ op: 'update', ...record }) + } + } + for (const uri of Object.keys(curr)) { + const record = curr[uri] + if (!checkout[uri]) { + ops.push({ op: 'delete', ...record }) + } + } + return ops +} + +const formatCheckout = ( + did: string, + contents: RepoContentsWithCids, +): Record => { + const records: Record = {} for (const collection of Object.keys(contents)) { for (const rkey of Object.keys(contents[collection])) { + const uri = AtUri.make(did, collection, rkey) const { cid, value } = contents[collection][rkey] - yield { collection, rkey, cid, record: value } + records[uri.toString()] = { + uri, + cid, + value, + } } } + return records } const needsHandleReindex = (actor: Actor | undefined, timestamp: string) => { diff --git a/packages/bsky/src/services/indexing/plugins/follow.ts b/packages/bsky/src/services/indexing/plugins/follow.ts index 29dcf88668b..883edb05c52 100644 --- a/packages/bsky/src/services/indexing/plugins/follow.ts +++ b/packages/bsky/src/services/indexing/plugins/follow.ts @@ -60,7 +60,7 @@ const notifsForInsert = (obj: IndexedFollow) => { recordCid: obj.cid, reason: 'follow' as const, reasonSubject: null, - sortAt: obj.indexedAt, + sortAt: obj.sortAt, }, ] } diff --git a/packages/bsky/src/services/indexing/plugins/like.ts b/packages/bsky/src/services/indexing/plugins/like.ts index 27a97a3c6a0..5f28a58063d 100644 --- a/packages/bsky/src/services/indexing/plugins/like.ts +++ b/packages/bsky/src/services/indexing/plugins/like.ts @@ -66,7 +66,7 @@ const notifsForInsert = (obj: IndexedLike) => { recordCid: obj.cid, reason: 'like' as const, reasonSubject: subjectUri.toString(), - sortAt: obj.indexedAt, + sortAt: obj.sortAt, }, ] } diff --git a/packages/bsky/src/services/indexing/plugins/post.ts b/packages/bsky/src/services/indexing/plugins/post.ts index 7e2900e41e1..64e8e2c79e0 100644 --- a/packages/bsky/src/services/indexing/plugins/post.ts +++ b/packages/bsky/src/services/indexing/plugins/post.ts @@ -35,7 +35,7 @@ type PostDescendent = { depth: number cid: string creator: string - indexedAt: string + sortAt: string } type IndexedPost = { post: Post @@ -164,7 +164,7 @@ const insertFn = async ( .selectFrom('descendent') .innerJoin('post', 'post.uri', 'descendent.uri') .selectAll('descendent') - .select(['cid', 'creator', 'indexedAt']) + .select(['cid', 'creator', 'sortAt']) .execute() return { post: insertedPost, @@ -196,7 +196,7 @@ const notifsForInsert = (obj: IndexedPost) => { author: obj.post.creator, recordUri: obj.post.uri, recordCid: obj.post.cid, - sortAt: obj.post.indexedAt, + sortAt: obj.post.sortAt, }) } } @@ -211,7 +211,7 @@ const notifsForInsert = (obj: IndexedPost) => { author: obj.post.creator, recordUri: obj.post.uri, recordCid: obj.post.cid, - sortAt: obj.post.indexedAt, + sortAt: obj.post.sortAt, }) } } @@ -228,7 +228,7 @@ const notifsForInsert = (obj: IndexedPost) => { author: obj.post.creator, recordUri: obj.post.uri, recordCid: obj.post.cid, - sortAt: obj.post.indexedAt, + sortAt: obj.post.sortAt, }) } } @@ -247,7 +247,7 @@ const notifsForInsert = (obj: IndexedPost) => { author: descendent.creator, recordUri: descendent.uri, recordCid: descendent.cid, - sortAt: descendent.indexedAt, + sortAt: descendent.sortAt, }) } } diff --git a/packages/bsky/src/services/indexing/plugins/repost.ts b/packages/bsky/src/services/indexing/plugins/repost.ts index 7817f881e2f..8cbebd4d8a2 100644 --- a/packages/bsky/src/services/indexing/plugins/repost.ts +++ b/packages/bsky/src/services/indexing/plugins/repost.ts @@ -85,7 +85,7 @@ const notifsForInsert = (obj: IndexedRepost) => { recordCid: obj.cid, reason: 'repost' as const, reasonSubject: subjectUri.toString(), - sortAt: obj.indexedAt, + sortAt: obj.sortAt, }, ] } diff --git a/packages/bsky/src/services/indexing/processor.ts b/packages/bsky/src/services/indexing/processor.ts index 5730f3ac97b..670926db887 100644 --- a/packages/bsky/src/services/indexing/processor.ts +++ b/packages/bsky/src/services/indexing/processor.ts @@ -64,7 +64,13 @@ export class RecordProcessor { lexicons.assertValidRecord(this.params.lexId, obj) } - async insertRecord(uri: AtUri, cid: CID, obj: unknown, timestamp: string) { + async insertRecord( + uri: AtUri, + cid: CID, + obj: unknown, + timestamp: string, + opts?: { disableNotifs?: boolean }, + ) { this.assertValidRecord(obj) await this.db .insertInto('record') @@ -86,7 +92,9 @@ export class RecordProcessor { ) if (inserted) { this.aggregateOnCommit(inserted) - await this.handleNotifs({ inserted }) + if (!opts?.disableNotifs) { + await this.handleNotifs({ inserted }) + } return } // if duplicate, insert into duplicates table with no events @@ -109,7 +117,13 @@ export class RecordProcessor { // for the uri then replace it. The main upside is that this allows the indexer // for each collection to avoid bespoke logic for in-place updates, which isn't // straightforward in the general case. We still get nice control over notifications. - async updateRecord(uri: AtUri, cid: CID, obj: unknown, timestamp: string) { + async updateRecord( + uri: AtUri, + cid: CID, + obj: unknown, + timestamp: string, + opts?: { disableNotifs?: boolean }, + ) { this.assertValidRecord(obj) await this.db .updateTable('record') @@ -158,7 +172,9 @@ export class RecordProcessor { ) } this.aggregateOnCommit(inserted) - await this.handleNotifs({ inserted, deleted }) + if (!opts?.disableNotifs) { + await this.handleNotifs({ inserted, deleted }) + } } async deleteRecord(uri: AtUri, cascading = false) { From e1b69f37b2d3edd28f45c67ef3dd89b79bed0469 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 24 Aug 2023 12:29:32 -0700 Subject: [PATCH 189/237] Notifications improvements (#1512) * Add collapse keys to notifications * Ensure stop processing after a notification result has been added * Simplify the collapse key to the notif reason * Update test * Fix tests * build branch * Tune notif rate limit to dramatically reduce engagement types but always deliver conversation types * dont build branch --------- Co-authored-by: dholms --- packages/bsky/src/notifications.ts | 32 +++++++++++++------ .../bsky/tests/notification-server.test.ts | 7 +++- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/packages/bsky/src/notifications.ts b/packages/bsky/src/notifications.ts index 854ba64e2d1..89a1ab7925c 100644 --- a/packages/bsky/src/notifications.ts +++ b/packages/bsky/src/notifications.ts @@ -22,18 +22,22 @@ type PushNotification = { data?: { [key: string]: string } + collapse_id?: string + collapse_key?: string } type InsertableNotif = Insertable type NotifDisplay = { + key: string + rateLimit: boolean title: string body: string notif: InsertableNotif } export class NotificationServer { - private rateLimiter = new RateLimiter(20, 20 * MINUTE) + private rateLimiter = new RateLimiter(1, 30 * MINUTE) constructor(public db: Database, public pushEndpoint?: string) {} @@ -52,6 +56,7 @@ export class NotificationServer { } async prepareNotifsToSend(notifications: InsertableNotif[]) { + const now = Date.now() const notifsToSend: PushNotification[] = [] const tokensByDid = await this.getTokensByDid( unique(notifications.map((n) => n.did)), @@ -69,6 +74,9 @@ export class NotificationServer { const userTokens = tokensByDid[userDid] ?? [] for (const t of userTokens) { const { appId, platform, token } = t + if (notifView.rateLimit && !this.rateLimiter.check(token, now)) { + continue + } if (platform === 'ios' || platform === 'android') { notifsToSend.push({ tokens: [token], @@ -81,6 +89,8 @@ export class NotificationServer { recordUri: notifView.notif.recordUri, recordCid: notifView.notif.recordCid, }, + collapse_id: notifView.key, + collapse_key: notifView.key, }) } else { // @TODO: Handle web notifs @@ -100,11 +110,7 @@ export class NotificationServer { * @returns void */ async processNotifications(notifs: PushNotification[]) { - const now = Date.now() - const permittedNotifs = notifs.filter((n) => - n.tokens.every((token) => this.rateLimiter.check(token, now)), - ) - for (const batch of chunkArray(permittedNotifs, 20)) { + for (const batch of chunkArray(notifs, 20)) { try { await this.sendPushNotifications(batch) } catch (err) { @@ -218,7 +224,7 @@ export class NotificationServer { const { author: authorDid, reason, - reasonSubject: subjectUri, // if like/reply/quote/emtion, the post which was liked/replied to/mention is in/or quoted. if custom feed liked, the feed which was liked + reasonSubject: subjectUri, // if like/reply/quote/mention, the post which was liked/replied to/mention is in/or quoted. if custom feed liked, the feed which was liked recordUri, } = notif @@ -237,15 +243,18 @@ export class NotificationServer { // if follow, get the URI of the author's profile // if reply, or mention, get URI of the postRecord // if like, or custom feed like, or repost get the URI of the reasonSubject + const key = reason let title = '' let body = '' + let rateLimit = true // check follow first and mention first because they don't have subjectUri and return // reply has subjectUri but the recordUri is the replied post if (reason === 'follow') { title = 'New follower!' body = `${author} has followed you` - results.push({ title, body, notif }) + results.push({ key, title, body, notif, rateLimit }) + continue } else if (reason === 'mention' || reason === 'reply') { // use recordUri for mention and reply title = @@ -253,7 +262,9 @@ export class NotificationServer { ? `${author} mentioned you` : `${author} replied to your post` body = postRecord?.text || '' - results.push({ title, body, notif }) + rateLimit = false // always deliver + results.push({ key, title, body, notif, rateLimit }) + continue } // if no subjectUri, don't send notification @@ -274,6 +285,7 @@ export class NotificationServer { } else if (reason === 'quote') { title = `${author} quoted your post` body = postSubject?.text || '' + rateLimit = true // always deliver } else if (reason === 'repost') { title = `${author} reposted your post` body = postSubject?.text || '' @@ -287,7 +299,7 @@ export class NotificationServer { continue } - results.push({ title, body, notif }) + results.push({ key, title, body, notif, rateLimit }) } return results diff --git a/packages/bsky/tests/notification-server.test.ts b/packages/bsky/tests/notification-server.test.ts index 55aa30029b4..c18f4b96e3e 100644 --- a/packages/bsky/tests/notification-server.test.ts +++ b/packages/bsky/tests/notification-server.test.ts @@ -115,10 +115,15 @@ describe('notification server', () => { const db = network.bsky.ctx.db.getPrimary() const notif = await getLikeNotification(db, alice) if (!notif) throw new Error('no notification found') - const notifAsArray = [notif] + const notifAsArray = [ + notif, + notif /* second one will get dropped by rate limit */, + ] const prepared = await notifServer.prepareNotifsToSend(notifAsArray) expect(prepared).toEqual([ { + collapse_id: 'like', + collapse_key: 'like', data: { reason: notif.reason, recordCid: notif.recordCid, From 19d2bdc4576bfabe6609afe160cc2c220c351579 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Thu, 24 Aug 2023 18:10:59 -0400 Subject: [PATCH 190/237] Stop using post hierarchy in pds (#1357) * serve pds post threads w/o post_hierarchy, continue to index post_hierarchy * add missing files --------- Co-authored-by: dholms --- .../api/app/bsky/feed/getPostThread.ts | 42 ++++++----- .../pds/src/app-view/services/feed/util.ts | 65 +++++++++++++++++ .../services/indexing/plugins/post.ts | 37 +++++----- ...230824T182048120Z-remove-post-hierarchy.ts | 36 ++++++++++ packages/pds/src/db/migrations/index.ts | 1 + .../tests/migrations/post-hierarchy.test.ts | 70 ------------------- packages/pds/tests/views/thread.test.ts | 61 +--------------- 7 files changed, 148 insertions(+), 164 deletions(-) create mode 100644 packages/pds/src/app-view/services/feed/util.ts create mode 100644 packages/pds/src/db/migrations/20230824T182048120Z-remove-post-hierarchy.ts delete mode 100644 packages/pds/tests/migrations/post-hierarchy.test.ts diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts index 7a39f4f4889..6b6ed6998e6 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts @@ -12,6 +12,10 @@ import { PostInfoMap, PostBlocksMap, } from '../../../../services/feed' +import { + getAncestorsAndSelfQb, + getDescendentsQb, +} from '../../../../services/feed/util' import { Labels } from '../../../../services/label' import { BlockedPost, @@ -91,12 +95,7 @@ export default function (server: Server, ctx: AppContext) { const feedService = ctx.services.appView.feed(ctx.db) const labelService = ctx.services.appView.label(ctx.db) - const threadData = await getThreadData( - feedService, - uri, - depth, - parentHeight, - ) + const threadData = await getThreadData(ctx, uri, depth, parentHeight) if (!threadData) { throw new InvalidRequestError(`Post not found: ${uri}`, 'NotFound') } @@ -246,24 +245,31 @@ const getRelevantIds = ( } const getThreadData = async ( - feedService: FeedService, + ctx: AppContext, uri: string, depth: number, parentHeight: number, ): Promise => { + const feedService = ctx.services.appView.feed(ctx.db) const [parents, children] = await Promise.all([ - feedService - .selectPostQb() - .innerJoin('post_hierarchy', 'post_hierarchy.ancestorUri', 'post.uri') - .where('post_hierarchy.uri', '=', uri) + getAncestorsAndSelfQb(ctx.db.db, { uri, parentHeight }) + .selectFrom('ancestor') + .innerJoin( + feedService.selectPostQb().as('post'), + 'post.uri', + 'ancestor.uri', + ) + .selectAll('post') .execute(), - feedService - .selectPostQb() - .innerJoin('post_hierarchy', 'post_hierarchy.uri', 'post.uri') - .where('post_hierarchy.uri', '!=', uri) - .where('post_hierarchy.ancestorUri', '=', uri) - .where('depth', '<=', depth) - .orderBy('post.createdAt', 'desc') + getDescendentsQb(ctx.db.db, { uri, depth }) + .selectFrom('descendent') + .innerJoin( + feedService.selectPostQb().as('post'), + 'post.uri', + 'descendent.uri', + ) + .selectAll('post') + .orderBy('sortAt', 'desc') .execute(), ]) const parentsByUri = parents.reduce((acc, parent) => { diff --git a/packages/pds/src/app-view/services/feed/util.ts b/packages/pds/src/app-view/services/feed/util.ts new file mode 100644 index 00000000000..cedc82df0e9 --- /dev/null +++ b/packages/pds/src/app-view/services/feed/util.ts @@ -0,0 +1,65 @@ +import { sql } from 'kysely' +import DatabaseSchema from '../../../db/database-schema' + +export const getDescendentsQb = ( + db: DatabaseSchema, + opts: { + uri: string + depth: number // required, protects against cycles + }, +) => { + const { uri, depth } = opts + const query = db.withRecursive('descendent(uri, depth)', (cte) => { + return cte + .selectFrom('post') + .select(['post.uri as uri', sql`1`.as('depth')]) + .where(sql`1`, '<=', depth) + .where('replyParent', '=', uri) + .unionAll( + cte + .selectFrom('post') + .innerJoin('descendent', 'descendent.uri', 'post.replyParent') + .where('descendent.depth', '<', depth) + .select([ + 'post.uri as uri', + sql`descendent.depth + 1`.as('depth'), + ]), + ) + }) + return query +} + +export const getAncestorsAndSelfQb = ( + db: DatabaseSchema, + opts: { + uri: string + parentHeight: number // required, protects against cycles + }, +) => { + const { uri, parentHeight } = opts + const query = db.withRecursive( + 'ancestor(uri, ancestorUri, height)', + (cte) => { + return cte + .selectFrom('post') + .select([ + 'post.uri as uri', + 'post.replyParent as ancestorUri', + sql`0`.as('height'), + ]) + .where('uri', '=', uri) + .unionAll( + cte + .selectFrom('post') + .innerJoin('ancestor', 'ancestor.ancestorUri', 'post.uri') + .where('ancestor.height', '<', parentHeight) + .select([ + 'post.uri as uri', + 'post.replyParent as ancestorUri', + sql`ancestor.height + 1`.as('height'), + ]), + ) + }, + ) + return query +} diff --git a/packages/pds/src/app-view/services/indexing/plugins/post.ts b/packages/pds/src/app-view/services/indexing/plugins/post.ts index 52db4821efb..f3088debac4 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/post.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/post.ts @@ -18,24 +18,30 @@ import { } from '../../../../db/database-schema' import { BackgroundQueue } from '../../../../event-stream/background-queue' import RecordProcessor from '../processor' -import { PostHierarchy } from '../../../db/tables/post-hierarchy' import { UserNotification } from '../../../../db/tables/user-notification' import { countAll, excluded } from '../../../../db/util' import { toSimplifiedISOSafe } from '../util' +import { getAncestorsAndSelfQb } from '../../feed/util' type Post = DatabaseSchemaType['post'] type PostEmbedImage = DatabaseSchemaType['post_embed_image'] type PostEmbedExternal = DatabaseSchemaType['post_embed_external'] type PostEmbedRecord = DatabaseSchemaType['post_embed_record'] +type PostAncestor = { + uri: string + height: number +} type IndexedPost = { post: Post facets: { type: 'mention' | 'link'; value: string }[] embeds?: (PostEmbedImage[] | PostEmbedExternal | PostEmbedRecord)[] - ancestors: PostHierarchy[] + ancestors?: PostAncestor[] } const lexId = lex.ids.AppBskyFeedPost +const REPLY_NOTIF_DEPTH = 5 + const insertFn = async ( db: DatabaseSchema, uri: AtUri, @@ -134,6 +140,7 @@ const insertFn = async ( } } // Thread index + // @TODO remove thread hierarchy indexing await db .insertInto('post_hierarchy') .values({ @@ -143,9 +150,8 @@ const insertFn = async ( }) .onConflict((oc) => oc.doNothing()) // Supports post updates .execute() - let ancestors: PostHierarchy[] = [] if (post.replyParent) { - ancestors = await db + await db .insertInto('post_hierarchy') .columns(['uri', 'ancestorUri', 'depth']) .expression( @@ -162,6 +168,13 @@ const insertFn = async ( .returningAll() .execute() } + const ancestors = await getAncestorsAndSelfQb(db, { + uri: post.uri, + parentHeight: REPLY_NOTIF_DEPTH, + }) + .selectFrom('ancestor') + .selectAll() + .execute() return { post: insertedPost, facets, embeds, ancestors } } @@ -207,10 +220,10 @@ const notifsForInsert = (obj: IndexedPost) => { } } } - const ancestors = [...obj.ancestors].sort((a, b) => a.depth - b.depth) - for (const relation of ancestors) { - if (relation.depth < 5) { - const ancestorUri = new AtUri(relation.ancestorUri) + for (const ancestor of obj.ancestors ?? []) { + if (ancestor.uri === obj.post.uri) continue // no need to notify for own post + if (ancestor.height < REPLY_NOTIF_DEPTH) { + const ancestorUri = new AtUri(ancestor.uri) maybeNotify({ userDid: ancestorUri.host, reason: 'reply', @@ -269,19 +282,11 @@ const deleteFn = async ( if (deletedPosts) { deletedEmbeds.push(deletedPosts) } - // Do not delete, maintain thread hierarchy even if post no longer exists - const ancestors = await db - .selectFrom('post_hierarchy') - .where('uri', '=', uriStr) - .where('depth', '>', 0) - .selectAll() - .execute() return deleted ? { post: deleted, facets: [], // Not used embeds: deletedEmbeds, - ancestors, } : null } diff --git a/packages/pds/src/db/migrations/20230824T182048120Z-remove-post-hierarchy.ts b/packages/pds/src/db/migrations/20230824T182048120Z-remove-post-hierarchy.ts new file mode 100644 index 00000000000..0c26b7da10d --- /dev/null +++ b/packages/pds/src/db/migrations/20230824T182048120Z-remove-post-hierarchy.ts @@ -0,0 +1,36 @@ +import { Kysely, sql } from 'kysely' + +export async function up(db: Kysely): Promise { + /* @TODO these migrations will be run manually in prod + await db.schema.dropTable('post_hierarchy').execute() + // recreate index that calculates e.g. "replyCount", turning it into a covering index + // for uri so that recursive query for post descendents can use an index-only scan. + await sql`create index "post_replyparent_uri_idx" on "post" ("replyParent") include ("uri")`.execute( + db, + ) + await db.schema.dropIndex('post_replyparent_idx').execute() + */ +} + +export async function down(db: Kysely): Promise { + /* @TODO these migrations will be run manually in prod + await db.schema + .createTable('post_hierarchy') + .addColumn('uri', 'varchar', (col) => col.notNull()) + .addColumn('ancestorUri', 'varchar', (col) => col.notNull()) + .addColumn('depth', 'integer', (col) => col.notNull()) + .addPrimaryKeyConstraint('post_hierarchy_pkey', ['uri', 'ancestorUri']) + .execute() + await db.schema + .createIndex('post_hierarchy_ancestoruri_idx') + .on('post_hierarchy') + .column('ancestorUri') + .execute() + await db.schema.dropIndex('post_replyparent_uri_idx').execute() + await db.schema + .createIndex('post_replyparent_idx') + .on('post') + .column('replyParent') + .execute() + */ +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index 43b92b8d911..1702c47f389 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -62,3 +62,4 @@ export * as _20230807T035309811Z from './20230807T035309811Z-feed-item-delete-in export * as _20230808T172813122Z from './20230808T172813122Z-repo-rev' export * as _20230810T203412859Z from './20230810T203412859Z-action-duration' export * as _20230818T134357818Z from './20230818T134357818Z-runtime-flags' +export * as _20230824T182048120Z from './20230824T182048120Z-remove-post-hierarchy' diff --git a/packages/pds/tests/migrations/post-hierarchy.test.ts b/packages/pds/tests/migrations/post-hierarchy.test.ts deleted file mode 100644 index 3e4a0a4ff3d..00000000000 --- a/packages/pds/tests/migrations/post-hierarchy.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { AtpAgent } from '@atproto/api' -import { Database } from '../../src' -import { RecordRef, SeedClient } from '../seeds/client' -import usersSeed from '../seeds/users' -import threadSeed, { walk, item, Item } from '../seeds/thread' -import { CloseFn, runTestServer } from '../_util' - -describe.skip('post hierarchy migration', () => { - let db: Database - let close: CloseFn - let sc: SeedClient - const threads: Item[] = [ - item(1, [item(2, [item(3), item(4)])]), - item(5, [item(6), item(7, [item(9, [item(11)]), item(10)]), item(8)]), - item(12), - ] - - beforeAll(async () => { - const server = await runTestServer({ - dbPostgresSchema: 'migration_post_hierarchy', - }) - db = server.ctx.db - close = server.close - const agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await usersSeed(sc) - await threadSeed(sc, sc.dids.alice, threads) - await db.migrateToOrThrow('_20230208T081544325Z') // Down to before index exists - await db.migrateToLatestOrThrow() // Build index from migration - }) - - afterAll(async () => { - await close() - }) - - it('indexes full post thread hierarchy.', async () => { - let closureSize = 0 - const itemByUri: Record = {} - - const postsAndReplies = ([] as { text: string; ref: RecordRef }[]) - .concat(Object.values(sc.posts[sc.dids.alice])) - .concat(Object.values(sc.replies[sc.dids.alice])) - - await walk(threads, async (item, depth) => { - const post = postsAndReplies.find((p) => p.text === String(item.id)) - if (!post) throw new Error('Post not found') - itemByUri[post.ref.uriStr] = item - closureSize += depth + 1 - }) - - const hierarchy = await db.db - .selectFrom('post_hierarchy') - .selectAll() - .execute() - - expect(hierarchy.length).toEqual(closureSize) - - for (const relation of hierarchy) { - const item = itemByUri[relation.uri] - const ancestor = itemByUri[relation.ancestorUri] - let depth = -1 - await walk([ancestor], async (candidate, candidateDepth) => { - if (candidate === item) { - depth = candidateDepth - } - }) - expect(depth).toEqual(relation.depth) - } - }) -}) diff --git a/packages/pds/tests/views/thread.test.ts b/packages/pds/tests/views/thread.test.ts index 88a595a6fc0..1efee8b0526 100644 --- a/packages/pds/tests/views/thread.test.ts +++ b/packages/pds/tests/views/thread.test.ts @@ -2,7 +2,6 @@ import assert from 'assert' import AtpAgent, { AppBskyFeedGetPostThread } from '@atproto/api' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { isThreadViewPost } from '@atproto/api/src/client/types/app/bsky/feed/defs' -import { Database } from '../../src' import { runTestServer, forSnapshot, @@ -10,14 +9,12 @@ import { adminAuth, TestServerInfo, } from '../_util' -import { RecordRef, SeedClient } from '../seeds/client' +import { SeedClient } from '../seeds/client' import basicSeed from '../seeds/basic' -import threadSeed, { walk, item, Item } from '../seeds/thread' describe('pds thread views', () => { let server: TestServerInfo let agent: AtpAgent - let db: Database let close: CloseFn let sc: SeedClient @@ -30,7 +27,6 @@ describe('pds thread views', () => { server = await runTestServer({ dbPostgresSchema: 'views_thread', }) - db = server.ctx.db close = server.close agent = new AtpAgent({ service: server.url }) sc = new SeedClient(agent) @@ -457,59 +453,4 @@ describe('pds thread views', () => { ), ) }) - - it('builds post hierarchy index.', async () => { - const threads: Item[] = [ - item(1, [item(2, [item(3), item(4)])]), - item(5, [item(6), item(7, [item(9, [item(11)]), item(10)]), item(8)]), - item(12), - ] - await threadSeed(sc, sc.dids.alice, threads) - let closureSize = 0 - const itemByUri: Record = {} - - const postsAndReplies = ([] as { text: string; ref: RecordRef }[]) - .concat(Object.values(sc.posts[sc.dids.alice])) - .concat(Object.values(sc.replies[sc.dids.alice])) - .filter((p) => { - const id = parseInt(p.text, 10) - return 0 < id && id <= 12 - }) - - await walk(threads, async (item, depth) => { - const post = postsAndReplies.find((p) => p.text === String(item.id)) - if (!post) throw new Error('Post not found') - itemByUri[post.ref.uriStr] = item - closureSize += depth + 1 - }) - - const hierarchy = await db.db - .selectFrom('post_hierarchy') - .where( - 'uri', - 'in', - postsAndReplies.map((p) => p.ref.uriStr), - ) - .orWhere( - 'ancestorUri', - 'in', - postsAndReplies.map((p) => p.ref.uriStr), - ) - .selectAll() - .execute() - - expect(hierarchy.length).toEqual(closureSize) - - for (const relation of hierarchy) { - const item = itemByUri[relation.uri] - const ancestor = itemByUri[relation.ancestorUri] - let depth = -1 - await walk([ancestor], async (candidate, candidateDepth) => { - if (candidate === item) { - depth = candidateDepth - } - }) - expect(depth).toEqual(relation.depth) - } - }) }) From d28c9ab3de95df64117660a8677f901bf899ce30 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Fri, 25 Aug 2023 11:09:15 -0400 Subject: [PATCH 191/237] Speed-up a few hot queries on pds and bsky (#1518) * hit index for notif unread count in appview * fix index usage on pds account lookup by handle * add index to blob tempkey column to support some hot queries --- .../api/app/bsky/notification/getUnreadCount.ts | 14 ++++++-------- .../20230825T142507884Z-blob-tempkey-idx.ts | 13 +++++++++++++ packages/pds/src/db/migrations/index.ts | 1 + packages/pds/src/services/account/index.ts | 8 +++++++- 4 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 packages/pds/src/db/migrations/20230825T142507884Z-blob-tempkey-idx.ts diff --git a/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts b/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts index 2d041b6ddeb..c23d7683abe 100644 --- a/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts @@ -1,3 +1,4 @@ +import { sql } from 'kysely' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import { countAll, notSoftDeletedClause } from '../../../../db/util' @@ -22,15 +23,12 @@ export default function (server: Server, ctx: AppContext) { .innerJoin('record', 'record.uri', 'notification.recordUri') .where(notSoftDeletedClause(ref('actor'))) .where(notSoftDeletedClause(ref('record'))) + // Ensure to hit notification_did_sortat_idx, handling case where lastSeenNotifs is null. .where('notification.did', '=', requester) - .where((inner) => - inner - .where('actor_state.lastSeenNotifs', 'is', null) - .orWhereRef( - 'notification.sortAt', - '>', - 'actor_state.lastSeenNotifs', - ), + .where( + 'notification.sortAt', + '>', + sql`coalesce(${ref('actor_state.lastSeenNotifs')}, ${''})`, ) .executeTakeFirst() diff --git a/packages/pds/src/db/migrations/20230825T142507884Z-blob-tempkey-idx.ts b/packages/pds/src/db/migrations/20230825T142507884Z-blob-tempkey-idx.ts new file mode 100644 index 00000000000..42cb843661e --- /dev/null +++ b/packages/pds/src/db/migrations/20230825T142507884Z-blob-tempkey-idx.ts @@ -0,0 +1,13 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createIndex('blob_tempkey_idx') + .on('blob') + .column('tempKey') + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropIndex('blob_tempkey_idx').execute() +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index 1702c47f389..4f0f49751ab 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -63,3 +63,4 @@ export * as _20230808T172813122Z from './20230808T172813122Z-repo-rev' export * as _20230810T203412859Z from './20230810T203412859Z-action-duration' export * as _20230818T134357818Z from './20230818T134357818Z-runtime-flags' export * as _20230824T182048120Z from './20230824T182048120Z-remove-post-hierarchy' +export * as _20230825T142507884Z from './20230825T142507884Z-blob-tempkey-idx' diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index ecc3f6b9fa1..dd28af01df5 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -37,7 +37,13 @@ export class AccountService { if (handleOrDid.startsWith('did:')) { return qb.where('did_handle.did', '=', handleOrDid) } else { - return qb.where('did_handle.handle', '=', handleOrDid) + // lower() is a little hack to avoid using the handle trgm index here, which is slow. not sure why it was preferring + // the handle trgm index over the handle unique index. in any case, we end-up using did_handle_handle_lower_idx instead, which is fast. + return qb.where( + sql`lower(${ref('did_handle.handle')})`, + '=', + handleOrDid, + ) } }) .selectAll('user_account') From bd3966555f4b909182dc2ae4f5d1551d2b6ca908 Mon Sep 17 00:00:00 2001 From: bnewbold Date: Fri, 25 Aug 2023 08:18:10 -0700 Subject: [PATCH 192/237] `@atproto/syntax`: merge of `@atproto/identifier`, `uri`, and `nsid` (#1464) * move nsid package in to identifier * move uri package in to identifier * update packages to pull from identifier, not nsid or uri * rename @atproto/identifier to @atproto/syntax * update all refs from @atproto/identifer to @atproto/syntax --- packages/README.md | 4 +- packages/api/package.json | 2 +- packages/api/src/bsky-agent.ts | 2 +- packages/api/src/index.ts | 2 +- packages/bsky/package.json | 3 +- .../src/api/app/bsky/graph/muteActorList.ts | 2 +- packages/bsky/src/api/blob-resolver.ts | 2 +- .../atproto/admin/reverseModerationAction.ts | 2 +- .../com/atproto/admin/takeModerationAction.ts | 2 +- .../api/com/atproto/identity/resolveHandle.ts | 2 +- .../src/api/com/atproto/moderation/util.ts | 2 +- .../src/api/com/atproto/repo/getRecord.ts | 2 +- packages/bsky/src/feed-gen/index.ts | 2 +- packages/bsky/src/indexer/subscription.ts | 2 +- packages/bsky/src/labeler/base.ts | 2 +- packages/bsky/src/labeler/keyword.ts | 2 +- packages/bsky/src/services/actor/views.ts | 2 +- packages/bsky/src/services/feed/index.ts | 4 +- packages/bsky/src/services/indexing/index.ts | 2 +- .../src/services/indexing/plugins/block.ts | 2 +- .../indexing/plugins/feed-generator.ts | 2 +- .../src/services/indexing/plugins/follow.ts | 2 +- .../src/services/indexing/plugins/like.ts | 2 +- .../services/indexing/plugins/list-item.ts | 2 +- .../src/services/indexing/plugins/list.ts | 2 +- .../src/services/indexing/plugins/post.ts | 2 +- .../src/services/indexing/plugins/profile.ts | 2 +- .../src/services/indexing/plugins/repost.ts | 2 +- .../bsky/src/services/indexing/processor.ts | 2 +- packages/bsky/src/services/label/index.ts | 2 +- .../bsky/src/services/moderation/index.ts | 2 +- .../bsky/src/services/moderation/views.ts | 4 +- packages/bsky/tests/_util.ts | 2 +- packages/bsky/tests/duplicate-records.test.ts | 2 +- packages/bsky/tests/indexing.test.ts | 2 +- packages/bsky/tests/moderation.test.ts | 2 +- packages/bsky/tests/seeds/client.ts | 2 +- packages/dev-env/package.json | 2 +- packages/dev-env/src/mock/index.ts | 2 +- packages/identifier/package.json | 11 +- packages/identifier/src/index.ts | 27 +++- packages/lex-cli/package.json | 2 +- packages/lex-cli/src/codegen/client.ts | 2 +- packages/lex-cli/src/codegen/server.ts | 2 +- packages/lex-cli/src/codegen/util.ts | 2 +- packages/lexicon/package.json | 4 +- packages/lexicon/src/types.ts | 2 +- packages/lexicon/src/validators/formats.ts | 9 +- packages/nsid/package.json | 10 +- packages/nsid/src/index.ts | 117 +-------------- packages/pds/package.json | 3 +- .../src/api/com/atproto/admin/getRecord.ts | 2 +- .../atproto/admin/reverseModerationAction.ts | 2 +- .../com/atproto/admin/takeModerationAction.ts | 2 +- .../api/com/atproto/identity/resolveHandle.ts | 2 +- .../src/api/com/atproto/moderation/util.ts | 2 +- .../pds/src/api/com/atproto/repo/getRecord.ts | 2 +- .../src/api/com/atproto/repo/listRecords.ts | 2 +- .../pds/src/api/com/atproto/repo/putRecord.ts | 2 +- .../api/app/bsky/feed/getPostThread.ts | 2 +- .../api/app/bsky/graph/muteActorList.ts | 2 +- .../pds/src/app-view/services/feed/index.ts | 2 +- .../src/app-view/services/indexing/index.ts | 2 +- .../services/indexing/plugins/block.ts | 2 +- .../indexing/plugins/feed-generator.ts | 2 +- .../services/indexing/plugins/follow.ts | 2 +- .../services/indexing/plugins/like.ts | 2 +- .../services/indexing/plugins/list-item.ts | 2 +- .../services/indexing/plugins/list.ts | 2 +- .../services/indexing/plugins/post.ts | 2 +- .../services/indexing/plugins/profile.ts | 2 +- .../services/indexing/plugins/repost.ts | 2 +- .../app-view/services/indexing/processor.ts | 2 +- .../pds/src/app-view/services/label/index.ts | 2 +- packages/pds/src/content-reporter/index.ts | 2 +- packages/pds/src/event-stream/messages.ts | 2 +- packages/pds/src/feed-gen/index.ts | 2 +- packages/pds/src/handle/index.ts | 2 +- packages/pds/src/labeler/base.ts | 2 +- packages/pds/src/repo/prepare.ts | 2 +- packages/pds/src/repo/types.ts | 2 +- packages/pds/src/services/local/index.ts | 2 +- packages/pds/src/services/moderation/index.ts | 2 +- packages/pds/src/services/moderation/views.ts | 2 +- packages/pds/src/services/record/index.ts | 4 +- packages/pds/src/services/repo/blobs.ts | 2 +- packages/pds/src/services/repo/index.ts | 2 +- packages/pds/tests/_util.ts | 2 +- packages/pds/tests/crud.test.ts | 2 +- packages/pds/tests/duplicate-records.test.ts | 2 +- packages/pds/tests/handle-validation.test.ts | 2 +- packages/pds/tests/indexing.test.ts | 2 +- .../pds/tests/migrations/blob-creator.test.ts | 2 +- .../migrations/indexed-at-on-record.test.ts | 2 +- packages/pds/tests/moderation.test.ts | 2 +- packages/pds/tests/seeds/client.ts | 2 +- packages/pds/tests/sync/sync.test.ts | 2 +- .../pds/tests/views/admin/get-record.test.ts | 2 +- packages/repo/package.json | 2 +- packages/syntax/README.md | 61 ++++++++ packages/syntax/babel.config.js | 1 + packages/syntax/build.js | 22 +++ packages/syntax/jest.config.js | 6 + packages/syntax/package.json | 32 ++++ packages/syntax/src/aturi.ts | 136 +++++++++++++++++ .../src/aturi_validation.ts} | 17 ++- packages/{identifier => syntax}/src/did.ts | 0 packages/{identifier => syntax}/src/handle.ts | 0 packages/syntax/src/index.ts | 4 + packages/syntax/src/nsid.ts | 111 ++++++++++++++ .../tests/aturi.test.ts} | 0 .../{identifier => syntax}/tests/did.test.ts | 0 .../tests/handle.test.ts | 0 packages/{nsid => syntax}/tests/nsid.test.ts | 0 packages/syntax/tsconfig.build.json | 4 + packages/syntax/tsconfig.json | 12 ++ packages/syntax/update-pkg.js | 14 ++ packages/uri/package.json | 9 +- packages/uri/src/index.ts | 142 +----------------- tsconfig.json | 1 + 120 files changed, 567 insertions(+), 379 deletions(-) create mode 100644 packages/syntax/README.md create mode 100644 packages/syntax/babel.config.js create mode 100644 packages/syntax/build.js create mode 100644 packages/syntax/jest.config.js create mode 100644 packages/syntax/package.json create mode 100644 packages/syntax/src/aturi.ts rename packages/{uri/src/validation.ts => syntax/src/aturi_validation.ts} (90%) rename packages/{identifier => syntax}/src/did.ts (100%) rename packages/{identifier => syntax}/src/handle.ts (100%) create mode 100644 packages/syntax/src/index.ts create mode 100644 packages/syntax/src/nsid.ts rename packages/{uri/tests/uri.test.ts => syntax/tests/aturi.test.ts} (100%) rename packages/{identifier => syntax}/tests/did.test.ts (100%) rename packages/{identifier => syntax}/tests/handle.test.ts (100%) rename packages/{nsid => syntax}/tests/nsid.test.ts (100%) create mode 100644 packages/syntax/tsconfig.build.json create mode 100644 packages/syntax/tsconfig.json create mode 100644 packages/syntax/update-pkg.js diff --git a/packages/README.md b/packages/README.md index 4f2a8bdd972..3d8c577d0c6 100644 --- a/packages/README.md +++ b/packages/README.md @@ -11,11 +11,9 @@ - [API](./api): A library for communicating with atproto servers. - [Common](./common): A library containing code which is shared between atproto packages. - [Crypto](./crypto): Atproto's common cryptographic operations. -- [Identity](./identity): A library for resolving atproto DIDs and handles. +- [Syntax](./syntax): A library for identifier syntax: NSID, AT URI, handles, etc. - [Lexicon](./lexicon): A library for validating data using atproto's schema system. -- [NSID](./nsid): A parser and generator of NSIDs. - [Repo](./repo): The "atproto repository" core implementation (a Merkle Search Tree). -- [URI](./uri): A parser and generator of `at://` uris. - [XRPC](./xrpc): An XRPC client implementation. - [XRPC Server](./xrpc-server): An XRPC server implementation. diff --git a/packages/api/package.json b/packages/api/package.json index b4797a0bc02..5d313b4666d 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@atproto/common-web": "*", - "@atproto/uri": "*", + "@atproto/syntax": "*", "@atproto/xrpc": "*", "tlds": "^1.234.0", "typed-emitter": "^2.1.0" diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 95f9dc82715..6122934d23e 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { AtpAgent } from './agent' import { AppBskyFeedPost, diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 33b48a2cf40..958e8930603 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,4 +1,4 @@ -export { AtUri } from '@atproto/uri' +export { AtUri } from '@atproto/syntax' export { BlobRef, lexToJson, diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 15d8e7e7644..35079fb4238 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -33,11 +33,10 @@ "@atproto/api": "*", "@atproto/common": "*", "@atproto/crypto": "*", - "@atproto/identifier": "*", + "@atproto/syntax": "*", "@atproto/identity": "*", "@atproto/lexicon": "*", "@atproto/repo": "*", - "@atproto/uri": "*", "@atproto/xrpc-server": "*", "@did-plc/lib": "^0.0.1", "@isaacs/ttlcache": "^1.4.1", diff --git a/packages/bsky/src/api/app/bsky/graph/muteActorList.ts b/packages/bsky/src/api/app/bsky/graph/muteActorList.ts index bece0291c94..b6b29796c5c 100644 --- a/packages/bsky/src/api/app/bsky/graph/muteActorList.ts +++ b/packages/bsky/src/api/app/bsky/graph/muteActorList.ts @@ -2,7 +2,7 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import * as lex from '../../../../lexicon/lexicons' import AppContext from '../../../../context' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.muteActorList({ diff --git a/packages/bsky/src/api/blob-resolver.ts b/packages/bsky/src/api/blob-resolver.ts index 622590e10b4..c366583c246 100644 --- a/packages/bsky/src/api/blob-resolver.ts +++ b/packages/bsky/src/api/blob-resolver.ts @@ -3,7 +3,7 @@ import express from 'express' import createError from 'http-errors' import axios, { AxiosError } from 'axios' import { CID } from 'multiformats/cid' -import { ensureValidDid } from '@atproto/identifier' +import { ensureValidDid } from '@atproto/syntax' import { forwardStreamErrors, VerifyCidTransform } from '@atproto/common' import { IdResolver, DidNotFoundError } from '@atproto/identity' import { TAKEDOWN } from '../lexicon/types/com/atproto/admin/defs' diff --git a/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts b/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts index 0e518f44567..e0c70103359 100644 --- a/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts +++ b/packages/bsky/src/api/com/atproto/admin/reverseModerationAction.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { ACKNOWLEDGE, diff --git a/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts b/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts index f86e0ae0fd1..fc49a9c14ff 100644 --- a/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts +++ b/packages/bsky/src/api/com/atproto/admin/takeModerationAction.ts @@ -1,5 +1,5 @@ import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' diff --git a/packages/bsky/src/api/com/atproto/identity/resolveHandle.ts b/packages/bsky/src/api/com/atproto/identity/resolveHandle.ts index 20865a058df..30c1d7f8a6f 100644 --- a/packages/bsky/src/api/com/atproto/identity/resolveHandle.ts +++ b/packages/bsky/src/api/com/atproto/identity/resolveHandle.ts @@ -1,5 +1,5 @@ import { InvalidRequestError } from '@atproto/xrpc-server' -import * as ident from '@atproto/identifier' +import * as ident from '@atproto/syntax' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' diff --git a/packages/bsky/src/api/com/atproto/moderation/util.ts b/packages/bsky/src/api/com/atproto/moderation/util.ts index 6aca2271506..d856148ee08 100644 --- a/packages/bsky/src/api/com/atproto/moderation/util.ts +++ b/packages/bsky/src/api/com/atproto/moderation/util.ts @@ -1,6 +1,6 @@ import { CID } from 'multiformats/cid' import { InvalidRequestError } from '@atproto/xrpc-server' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { ModerationAction } from '../../../../db/tables/moderation' import { ModerationReport } from '../../../../db/tables/moderation' import { InputSchema as ReportInput } from '../../../../lexicon/types/com/atproto/moderation/createReport' diff --git a/packages/bsky/src/api/com/atproto/repo/getRecord.ts b/packages/bsky/src/api/com/atproto/repo/getRecord.ts index f96da38cdf6..c42c1fd6b4c 100644 --- a/packages/bsky/src/api/com/atproto/repo/getRecord.ts +++ b/packages/bsky/src/api/com/atproto/repo/getRecord.ts @@ -1,5 +1,5 @@ import { InvalidRequestError } from '@atproto/xrpc-server' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { jsonStringToLex } from '@atproto/lexicon' diff --git a/packages/bsky/src/feed-gen/index.ts b/packages/bsky/src/feed-gen/index.ts index c05f8cb72d3..d00d22c59d9 100644 --- a/packages/bsky/src/feed-gen/index.ts +++ b/packages/bsky/src/feed-gen/index.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { ids } from '../lexicon/lexicons' import withFriends from './with-friends' import bskyTeam from './bsky-team' diff --git a/packages/bsky/src/indexer/subscription.ts b/packages/bsky/src/indexer/subscription.ts index dc7fdd4ff80..76c9b7297df 100644 --- a/packages/bsky/src/indexer/subscription.ts +++ b/packages/bsky/src/indexer/subscription.ts @@ -1,6 +1,6 @@ import assert from 'node:assert' import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { cborDecode, wait } from '@atproto/common' import { DisconnectError } from '@atproto/xrpc-server' import { diff --git a/packages/bsky/src/labeler/base.ts b/packages/bsky/src/labeler/base.ts index 7e792310f4a..f9efd457fdb 100644 --- a/packages/bsky/src/labeler/base.ts +++ b/packages/bsky/src/labeler/base.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { AtpAgent } from '@atproto/api' import { cidForRecord } from '@atproto/repo' import { dedupe, getFieldsFromRecord } from './util' diff --git a/packages/bsky/src/labeler/keyword.ts b/packages/bsky/src/labeler/keyword.ts index 60f002fa6c3..395bb15278e 100644 --- a/packages/bsky/src/labeler/keyword.ts +++ b/packages/bsky/src/labeler/keyword.ts @@ -3,7 +3,7 @@ import { Labeler } from './base' import { getFieldsFromRecord, keywordLabeling } from './util' import { IdResolver } from '@atproto/identity' import { BackgroundQueue } from '../background' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { IndexerConfig } from '../indexer/config' export class KeywordLabeler extends Labeler { diff --git a/packages/bsky/src/services/actor/views.ts b/packages/bsky/src/services/actor/views.ts index 6794b823fa1..ff90a6be2c5 100644 --- a/packages/bsky/src/services/actor/views.ts +++ b/packages/bsky/src/services/actor/views.ts @@ -1,5 +1,5 @@ import { mapDefined } from '@atproto/common' -import { INVALID_HANDLE } from '@atproto/identifier' +import { INVALID_HANDLE } from '@atproto/syntax' import { jsonStringToLex } from '@atproto/lexicon' import { ProfileViewDetailed, diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index 09fa32e74d2..895cec08c7b 100644 --- a/packages/bsky/src/services/feed/index.ts +++ b/packages/bsky/src/services/feed/index.ts @@ -1,7 +1,7 @@ import { sql } from 'kysely' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { dedupeStrs } from '@atproto/common' -import { INVALID_HANDLE } from '@atproto/identifier' +import { INVALID_HANDLE } from '@atproto/syntax' import { jsonStringToLex } from '@atproto/lexicon' import { Database } from '../../db' import { diff --git a/packages/bsky/src/services/indexing/index.ts b/packages/bsky/src/services/indexing/index.ts index fb5e4ec167e..025686d90ae 100644 --- a/packages/bsky/src/services/indexing/index.ts +++ b/packages/bsky/src/services/indexing/index.ts @@ -9,7 +9,7 @@ import { RepoContentsWithCids, Commit, } from '@atproto/repo' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { IdResolver, getPds } from '@atproto/identity' import { DAY, HOUR } from '@atproto/common' import { ValidationError } from '@atproto/lexicon' diff --git a/packages/bsky/src/services/indexing/plugins/block.ts b/packages/bsky/src/services/indexing/plugins/block.ts index 515166735e1..c0c8fe9d770 100644 --- a/packages/bsky/src/services/indexing/plugins/block.ts +++ b/packages/bsky/src/services/indexing/plugins/block.ts @@ -1,5 +1,5 @@ import { Selectable } from 'kysely' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { CID } from 'multiformats/cid' import * as Block from '../../../lexicon/types/app/bsky/graph/block' import * as lex from '../../../lexicon/lexicons' diff --git a/packages/bsky/src/services/indexing/plugins/feed-generator.ts b/packages/bsky/src/services/indexing/plugins/feed-generator.ts index 97b3baaed04..a0c32ff9129 100644 --- a/packages/bsky/src/services/indexing/plugins/feed-generator.ts +++ b/packages/bsky/src/services/indexing/plugins/feed-generator.ts @@ -1,5 +1,5 @@ import { Selectable } from 'kysely' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { CID } from 'multiformats/cid' import * as FeedGenerator from '../../../lexicon/types/app/bsky/feed/generator' import * as lex from '../../../lexicon/lexicons' diff --git a/packages/bsky/src/services/indexing/plugins/follow.ts b/packages/bsky/src/services/indexing/plugins/follow.ts index 883edb05c52..d6cf1996f98 100644 --- a/packages/bsky/src/services/indexing/plugins/follow.ts +++ b/packages/bsky/src/services/indexing/plugins/follow.ts @@ -1,5 +1,5 @@ import { Selectable } from 'kysely' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { CID } from 'multiformats/cid' import * as Follow from '../../../lexicon/types/app/bsky/graph/follow' import * as lex from '../../../lexicon/lexicons' diff --git a/packages/bsky/src/services/indexing/plugins/like.ts b/packages/bsky/src/services/indexing/plugins/like.ts index 5f28a58063d..7899b48b1fd 100644 --- a/packages/bsky/src/services/indexing/plugins/like.ts +++ b/packages/bsky/src/services/indexing/plugins/like.ts @@ -1,5 +1,5 @@ import { Selectable } from 'kysely' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { CID } from 'multiformats/cid' import * as Like from '../../../lexicon/types/app/bsky/feed/like' import * as lex from '../../../lexicon/lexicons' diff --git a/packages/bsky/src/services/indexing/plugins/list-item.ts b/packages/bsky/src/services/indexing/plugins/list-item.ts index 83ed29d9835..231fb761e16 100644 --- a/packages/bsky/src/services/indexing/plugins/list-item.ts +++ b/packages/bsky/src/services/indexing/plugins/list-item.ts @@ -1,5 +1,5 @@ import { Selectable } from 'kysely' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { CID } from 'multiformats/cid' import * as ListItem from '../../../lexicon/types/app/bsky/graph/listitem' import * as lex from '../../../lexicon/lexicons' diff --git a/packages/bsky/src/services/indexing/plugins/list.ts b/packages/bsky/src/services/indexing/plugins/list.ts index 6c2a46d003b..c74c09c274f 100644 --- a/packages/bsky/src/services/indexing/plugins/list.ts +++ b/packages/bsky/src/services/indexing/plugins/list.ts @@ -1,5 +1,5 @@ import { Selectable } from 'kysely' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { CID } from 'multiformats/cid' import * as List from '../../../lexicon/types/app/bsky/graph/list' import * as lex from '../../../lexicon/lexicons' diff --git a/packages/bsky/src/services/indexing/plugins/post.ts b/packages/bsky/src/services/indexing/plugins/post.ts index 64e8e2c79e0..7ce431fdcd8 100644 --- a/packages/bsky/src/services/indexing/plugins/post.ts +++ b/packages/bsky/src/services/indexing/plugins/post.ts @@ -1,6 +1,6 @@ import { Insertable, Selectable, sql } from 'kysely' import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { Record as PostRecord } from '../../../lexicon/types/app/bsky/feed/post' import { isMain as isEmbedImage } from '../../../lexicon/types/app/bsky/embed/images' import { isMain as isEmbedExternal } from '../../../lexicon/types/app/bsky/embed/external' diff --git a/packages/bsky/src/services/indexing/plugins/profile.ts b/packages/bsky/src/services/indexing/plugins/profile.ts index 68dec0ccfa6..ea0c8f07f98 100644 --- a/packages/bsky/src/services/indexing/plugins/profile.ts +++ b/packages/bsky/src/services/indexing/plugins/profile.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { CID } from 'multiformats/cid' import * as Profile from '../../../lexicon/types/app/bsky/actor/profile' import * as lex from '../../../lexicon/lexicons' diff --git a/packages/bsky/src/services/indexing/plugins/repost.ts b/packages/bsky/src/services/indexing/plugins/repost.ts index 8cbebd4d8a2..aa93d7b0f61 100644 --- a/packages/bsky/src/services/indexing/plugins/repost.ts +++ b/packages/bsky/src/services/indexing/plugins/repost.ts @@ -1,6 +1,6 @@ import { Selectable } from 'kysely' import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import * as Repost from '../../../lexicon/types/app/bsky/feed/repost' import * as lex from '../../../lexicon/lexicons' import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' diff --git a/packages/bsky/src/services/indexing/processor.ts b/packages/bsky/src/services/indexing/processor.ts index 670926db887..2a02c61125e 100644 --- a/packages/bsky/src/services/indexing/processor.ts +++ b/packages/bsky/src/services/indexing/processor.ts @@ -1,6 +1,6 @@ import { Insertable } from 'kysely' import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { jsonStringToLex, stringifyLex } from '@atproto/lexicon' import DatabaseSchema from '../../db/database-schema' import { lexicons } from '../../lexicon/lexicons' diff --git a/packages/bsky/src/services/label/index.ts b/packages/bsky/src/services/label/index.ts index 08970395795..42b263138ab 100644 --- a/packages/bsky/src/services/label/index.ts +++ b/packages/bsky/src/services/label/index.ts @@ -1,5 +1,5 @@ import { sql } from 'kysely' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { Database } from '../../db' import { Label, isSelfLabels } from '../../lexicon/types/com/atproto/label/defs' import { ids } from '../../lexicon/lexicons' diff --git a/packages/bsky/src/services/moderation/index.ts b/packages/bsky/src/services/moderation/index.ts index 5d935046867..0abf8f348eb 100644 --- a/packages/bsky/src/services/moderation/index.ts +++ b/packages/bsky/src/services/moderation/index.ts @@ -1,6 +1,6 @@ import { Selectable, sql } from 'kysely' import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { InvalidRequestError } from '@atproto/xrpc-server' import { PrimaryDatabase } from '../../db' import { ModerationAction, ModerationReport } from '../../db/tables/moderation' diff --git a/packages/bsky/src/services/moderation/views.ts b/packages/bsky/src/services/moderation/views.ts index f75005143c0..b8d745a594d 100644 --- a/packages/bsky/src/services/moderation/views.ts +++ b/packages/bsky/src/services/moderation/views.ts @@ -1,7 +1,7 @@ import { Selectable } from 'kysely' import { ArrayEl } from '@atproto/common' -import { AtUri } from '@atproto/uri' -import { INVALID_HANDLE } from '@atproto/identifier' +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' diff --git a/packages/bsky/tests/_util.ts b/packages/bsky/tests/_util.ts index 46a2eb5d901..4f08af9e0f6 100644 --- a/packages/bsky/tests/_util.ts +++ b/packages/bsky/tests/_util.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { lexToJson } from '@atproto/lexicon' import { CID } from 'multiformats/cid' import { diff --git a/packages/bsky/tests/duplicate-records.test.ts b/packages/bsky/tests/duplicate-records.test.ts index 5aab5d0a9c7..9c7617bd668 100644 --- a/packages/bsky/tests/duplicate-records.test.ts +++ b/packages/bsky/tests/duplicate-records.test.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { cidForCbor, TID } from '@atproto/common' import { WriteOpAction } from '@atproto/repo' import { TestNetwork } from '@atproto/dev-env' diff --git a/packages/bsky/tests/indexing.test.ts b/packages/bsky/tests/indexing.test.ts index a4b1d32afca..10913694dfe 100644 --- a/packages/bsky/tests/indexing.test.ts +++ b/packages/bsky/tests/indexing.test.ts @@ -3,7 +3,7 @@ import { CID } from 'multiformats/cid' import { cidForCbor, TID } from '@atproto/common' import * as pdsRepo from '@atproto/pds/src/repo/prepare' import { WriteOpAction } from '@atproto/repo' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import AtpAgent, { AppBskyActorProfile, AppBskyFeedPost, diff --git a/packages/bsky/tests/moderation.test.ts b/packages/bsky/tests/moderation.test.ts index 51561e191fa..109b576bb6f 100644 --- a/packages/bsky/tests/moderation.test.ts +++ b/packages/bsky/tests/moderation.test.ts @@ -1,7 +1,7 @@ import { TestNetwork } from '@atproto/dev-env' import { TID, cidForCbor } from '@atproto/common' import AtpAgent, { ComAtprotoAdminTakeModerationAction } from '@atproto/api' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { forSnapshot } from './_util' import { ImageRef, RecordRef, SeedClient } from './seeds/client' import basicSeed from './seeds/basic' diff --git a/packages/bsky/tests/seeds/client.ts b/packages/bsky/tests/seeds/client.ts index 946920b202d..ddd1acb9192 100644 --- a/packages/bsky/tests/seeds/client.ts +++ b/packages/bsky/tests/seeds/client.ts @@ -1,7 +1,7 @@ import fs from 'fs/promises' import { CID } from 'multiformats/cid' import AtpAgent from '@atproto/api' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { BlobRef } from '@atproto/lexicon' import { Main as Facet } from '@atproto/api/src/client/types/app/bsky/richtext/facet' import { InputSchema as TakeActionInput } from '@atproto/api/src/client/types/com/atproto/admin/takeModerationAction' diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index 179d1990004..57b0f0b7adf 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -29,9 +29,9 @@ "@atproto/api": "*", "@atproto/bsky": "*", "@atproto/crypto": "*", + "@atproto/syntax": "*", "@atproto/identity": "*", "@atproto/pds": "*", - "@atproto/uri": "*", "@atproto/xrpc-server": "*", "@did-plc/lib": "^0.0.1", "@did-plc/server": "^0.0.1", diff --git a/packages/dev-env/src/mock/index.ts b/packages/dev-env/src/mock/index.ts index a056d6c916e..afa661f7ecb 100644 --- a/packages/dev-env/src/mock/index.ts +++ b/packages/dev-env/src/mock/index.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import AtpAgent from '@atproto/api' import { REASONSPAM, diff --git a/packages/identifier/package.json b/packages/identifier/package.json index 1a7366c5f7e..6abfe06cedd 100644 --- a/packages/identifier/package.json +++ b/packages/identifier/package.json @@ -3,9 +3,9 @@ "version": "0.2.0", "main": "src/index.ts", "scripts": { - "test": "jest", - "prettier": "prettier --check src/ tests/", - "prettier:fix": "prettier --write src/ tests/", + "test": "true", + "prettier": "prettier --check src/", + "prettier:fix": "prettier --write src/", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "yarn lint --fix", "verify": "run-p prettier lint", @@ -24,9 +24,6 @@ "directory": "packages/identifier" }, "dependencies": { - "@atproto/common-web": "*" - }, - "browser": { - "dns/promises": false + "@atproto/syntax": "*" } } diff --git a/packages/identifier/src/index.ts b/packages/identifier/src/index.ts index 4f92a9d5180..a590a9da09c 100644 --- a/packages/identifier/src/index.ts +++ b/packages/identifier/src/index.ts @@ -1,2 +1,25 @@ -export * from './handle' -export * from './did' +export { + ATP_URI_REGEX, + AtUri, + DISALLOWED_TLDS, + DisallowedDomainError, + INVALID_HANDLE, + InvalidDidError, + InvalidHandleError, + InvalidNsidError, + NSID, + ReservedHandleError, + UnsupportedDomainError, + ensureValidAtUri, + ensureValidAtUriRegex, + ensureValidDid, + ensureValidDidRegex, + ensureValidHandle, + ensureValidHandleRegex, + ensureValidNsid, + ensureValidNsidRegex, + isValidHandle, + isValidTld, + normalizeAndEnsureValidHandle, + normalizeHandle, +} from '@atproto/syntax' diff --git a/packages/lex-cli/package.json b/packages/lex-cli/package.json index 1c5a0ca5960..c521a70339a 100644 --- a/packages/lex-cli/package.json +++ b/packages/lex-cli/package.json @@ -22,8 +22,8 @@ "directory": "packages/lex-cli" }, "dependencies": { + "@atproto/syntax": "*", "@atproto/lexicon": "*", - "@atproto/nsid": "*", "chalk": "^5.1.1", "commander": "^9.4.0", "ts-morph": "^16.0.0", diff --git a/packages/lex-cli/src/codegen/client.ts b/packages/lex-cli/src/codegen/client.ts index 06998ed4610..58feaf75ad2 100644 --- a/packages/lex-cli/src/codegen/client.ts +++ b/packages/lex-cli/src/codegen/client.ts @@ -11,7 +11,7 @@ import { LexXrpcQuery, LexRecord, } from '@atproto/lexicon' -import { NSID } from '@atproto/nsid' +import { NSID } from '@atproto/syntax' import { gen, utilTs, lexiconsTs } from './common' import { GeneratedAPI } from '../types' import { diff --git a/packages/lex-cli/src/codegen/server.ts b/packages/lex-cli/src/codegen/server.ts index c6f2d55cc78..8363f1630c6 100644 --- a/packages/lex-cli/src/codegen/server.ts +++ b/packages/lex-cli/src/codegen/server.ts @@ -12,7 +12,7 @@ import { LexRecord, LexXrpcSubscription, } from '@atproto/lexicon' -import { NSID } from '@atproto/nsid' +import { NSID } from '@atproto/syntax' import { gen, lexiconsTs, utilTs } from './common' import { GeneratedAPI } from '../types' import { diff --git a/packages/lex-cli/src/codegen/util.ts b/packages/lex-cli/src/codegen/util.ts index c7330fa276c..043821e4715 100644 --- a/packages/lex-cli/src/codegen/util.ts +++ b/packages/lex-cli/src/codegen/util.ts @@ -1,5 +1,5 @@ import { LexiconDoc, LexUserType } from '@atproto/lexicon' -import { NSID } from '@atproto/nsid' +import { NSID } from '@atproto/syntax' export interface DefTreeNodeUserType { nsid: string diff --git a/packages/lexicon/package.json b/packages/lexicon/package.json index afca71f9d50..306991073f3 100644 --- a/packages/lexicon/package.json +++ b/packages/lexicon/package.json @@ -25,9 +25,7 @@ }, "dependencies": { "@atproto/common-web": "*", - "@atproto/identifier": "*", - "@atproto/nsid": "*", - "@atproto/uri": "*", + "@atproto/syntax": "*", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.6.4", "zod": "^3.21.4" diff --git a/packages/lexicon/src/types.ts b/packages/lexicon/src/types.ts index c237e3c0730..98616c9e59a 100644 --- a/packages/lexicon/src/types.ts +++ b/packages/lexicon/src/types.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { NSID } from '@atproto/nsid' +import { NSID } from '@atproto/syntax' import { requiredPropertiesRefinement } from './util' // primitives diff --git a/packages/lexicon/src/validators/formats.ts b/packages/lexicon/src/validators/formats.ts index f94401ba6cb..63fc941628e 100644 --- a/packages/lexicon/src/validators/formats.ts +++ b/packages/lexicon/src/validators/formats.ts @@ -1,9 +1,12 @@ -import { ensureValidAtUri } from '@atproto/uri' import { isValidISODateString } from 'iso-datestring-validator' import { CID } from 'multiformats/cid' import { ValidationResult, ValidationError } from '../types' -import { ensureValidDid, ensureValidHandle } from '@atproto/identifier' -import { ensureValidNsid } from '@atproto/nsid' +import { + ensureValidDid, + ensureValidHandle, + ensureValidNsid, + ensureValidAtUri, +} from '@atproto/syntax' import { validateLanguage } from '@atproto/common-web' export function datetime(path: string, value: string): ValidationResult { diff --git a/packages/nsid/package.json b/packages/nsid/package.json index 201abfc43f0..211d222826b 100644 --- a/packages/nsid/package.json +++ b/packages/nsid/package.json @@ -3,9 +3,9 @@ "version": "0.1.0", "main": "src/index.ts", "scripts": { - "test": "jest", - "prettier": "prettier --check src/ tests/", - "prettier:fix": "prettier --write src/ tests/", + "test": "true", + "prettier": "prettier --check src/", + "prettier:fix": "prettier --write src/", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "yarn lint --fix", "verify": "run-p prettier lint", @@ -19,5 +19,7 @@ "url": "https://github.com/bluesky-social/atproto.git", "directory": "packages/nsid" }, - "dependencies": {} + "dependencies": { + "@atproto/syntax": "*" + } } diff --git a/packages/nsid/src/index.ts b/packages/nsid/src/index.ts index b436912bf7d..7a2efe52465 100644 --- a/packages/nsid/src/index.ts +++ b/packages/nsid/src/index.ts @@ -1,111 +1,6 @@ -/* -Grammar: - -alpha = "a" / "b" / "c" / "d" / "e" / "f" / "g" / "h" / "i" / "j" / "k" / "l" / "m" / "n" / "o" / "p" / "q" / "r" / "s" / "t" / "u" / "v" / "w" / "x" / "y" / "z" / "A" / "B" / "C" / "D" / "E" / "F" / "G" / "H" / "I" / "J" / "K" / "L" / "M" / "N" / "O" / "P" / "Q" / "R" / "S" / "T" / "U" / "V" / "W" / "X" / "Y" / "Z" -number = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" / "0" -delim = "." -segment = alpha *( alpha / number / "-" ) -authority = segment *( delim segment ) -name = alpha *( alpha ) -nsid = authority delim name - -*/ - -export class NSID { - segments: string[] = [] - - static parse(nsid: string): NSID { - return new NSID(nsid) - } - - static create(authority: string, name: string): NSID { - const segments = [...authority.split('.').reverse(), name].join('.') - return new NSID(segments) - } - - static isValid(nsid: string): boolean { - try { - NSID.parse(nsid) - return true - } catch (e) { - return false - } - } - - constructor(nsid: string) { - ensureValidNsid(nsid) - this.segments = nsid.split('.') - } - - get authority() { - return this.segments - .slice(0, this.segments.length - 1) - .reverse() - .join('.') - } - - get name() { - return this.segments.at(this.segments.length - 1) - } - - toString() { - return this.segments.join('.') - } -} - -// Human readable constraints on NSID: -// - a valid domain in reversed notation -// - followed by an additional period-separated name, which is camel-case letters -export const ensureValidNsid = (nsid: string): void => { - const toCheck = nsid - - // check that all chars are boring ASCII - if (!/^[a-zA-Z0-9.-]*$/.test(toCheck)) { - throw new InvalidNsidError( - 'Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)', - ) - } - - if (toCheck.length > 253 + 1 + 63) { - throw new InvalidNsidError('NSID is too long (317 chars max)') - } - const labels = toCheck.split('.') - if (labels.length < 3) { - throw new InvalidNsidError('NSID needs at least three parts') - } - for (let i = 0; i < labels.length; i++) { - const l = labels[i] - if (l.length < 1) { - throw new InvalidNsidError('NSID parts can not be empty') - } - if (l.length > 63) { - throw new InvalidNsidError('NSID part too long (max 63 chars)') - } - if (l.endsWith('-') || l.startsWith('-')) { - throw new InvalidNsidError('NSID parts can not start or end with hyphen') - } - if (/^[0-9]/.test(l) && i == 0) { - throw new InvalidNsidError('NSID first part may not start with a digit') - } - if (!/^[a-zA-Z]+$/.test(l) && i + 1 == labels.length) { - throw new InvalidNsidError('NSID name part must be only letters') - } - } -} - -export const ensureValidNsidRegex = (nsid: string): void => { - // simple regex to enforce most constraints via just regex and length. - // hand wrote this regex based on above constraints - if ( - !/^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z]{0,61}[a-zA-Z])?)$/.test( - nsid, - ) - ) { - throw new InvalidNsidError("NSID didn't validate via regex") - } - if (nsid.length > 253 + 1 + 63) { - throw new InvalidNsidError('NSID is too long (317 chars max)') - } -} - -export class InvalidNsidError extends Error {} +export { + NSID, + ensureValidNsid, + ensureValidNsidRegex, + InvalidNsidError, +} from '@atproto/syntax' diff --git a/packages/pds/package.json b/packages/pds/package.json index 5e0e12e8235..ce1f52fe464 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -35,11 +35,10 @@ "@atproto/api": "*", "@atproto/common": "*", "@atproto/crypto": "*", - "@atproto/identifier": "*", + "@atproto/syntax": "*", "@atproto/identity": "*", "@atproto/lexicon": "*", "@atproto/repo": "*", - "@atproto/uri": "*", "@atproto/xrpc-server": "*", "@did-plc/lib": "^0.0.1", "better-sqlite3": "^7.6.2", diff --git a/packages/pds/src/api/com/atproto/admin/getRecord.ts b/packages/pds/src/api/com/atproto/admin/getRecord.ts index 92489b1c1f0..4da9cf9246d 100644 --- a/packages/pds/src/api/com/atproto/admin/getRecord.ts +++ b/packages/pds/src/api/com/atproto/admin/getRecord.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' diff --git a/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts b/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts index e5f6bf7835f..d652b501166 100644 --- a/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/reverseModerationAction.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' diff --git a/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts b/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts index 9e9cea4ce5c..8a81245a9e8 100644 --- a/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts +++ b/packages/pds/src/api/com/atproto/admin/takeModerationAction.ts @@ -1,5 +1,5 @@ import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { diff --git a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts index 7bcf9498b6f..a4eead11e22 100644 --- a/packages/pds/src/api/com/atproto/identity/resolveHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/resolveHandle.ts @@ -1,6 +1,6 @@ import { AtpAgent } from '@atproto/api' import { InvalidRequestError } from '@atproto/xrpc-server' -import * as ident from '@atproto/identifier' +import * as ident from '@atproto/syntax' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' diff --git a/packages/pds/src/api/com/atproto/moderation/util.ts b/packages/pds/src/api/com/atproto/moderation/util.ts index b4e403f0fcc..89ee2f1ac92 100644 --- a/packages/pds/src/api/com/atproto/moderation/util.ts +++ b/packages/pds/src/api/com/atproto/moderation/util.ts @@ -1,5 +1,5 @@ import { InvalidRequestError } from '@atproto/xrpc-server' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { ModerationAction } from '../../../../db/tables/moderation' import { ModerationReport } from '../../../../db/tables/moderation' import { InputSchema as ReportInput } from '../../../../lexicon/types/com/atproto/moderation/createReport' diff --git a/packages/pds/src/api/com/atproto/repo/getRecord.ts b/packages/pds/src/api/com/atproto/repo/getRecord.ts index 99f5c8e18c2..2ce83916b32 100644 --- a/packages/pds/src/api/com/atproto/repo/getRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/getRecord.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' diff --git a/packages/pds/src/api/com/atproto/repo/listRecords.ts b/packages/pds/src/api/com/atproto/repo/listRecords.ts index c99be4677b2..8c2669ff010 100644 --- a/packages/pds/src/api/com/atproto/repo/listRecords.ts +++ b/packages/pds/src/api/com/atproto/repo/listRecords.ts @@ -1,5 +1,5 @@ import { InvalidRequestError } from '@atproto/xrpc-server' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' diff --git a/packages/pds/src/api/com/atproto/repo/putRecord.ts b/packages/pds/src/api/com/atproto/repo/putRecord.ts index b0ca8f9cd95..311083359f2 100644 --- a/packages/pds/src/api/com/atproto/repo/putRecord.ts +++ b/packages/pds/src/api/com/atproto/repo/putRecord.ts @@ -1,5 +1,5 @@ import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import { prepareUpdate, prepareCreate } from '../../../../repo' diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts index 6b6ed6998e6..5f570827635 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getPostThread.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { AppBskyFeedGetPostThread } from '@atproto/api' import { Headers } from '@atproto/xrpc' import { InvalidRequestError } from '@atproto/xrpc-server' diff --git a/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts b/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts index ba41fe8c52e..728aa2a5837 100644 --- a/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts +++ b/packages/pds/src/app-view/api/app/bsky/graph/muteActorList.ts @@ -1,7 +1,7 @@ import { Server } from '../../../../../lexicon' import * as lex from '../../../../../lexicon/lexicons' import AppContext from '../../../../../context' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { InvalidRequestError } from '@atproto/xrpc-server' export default function (server: Server, ctx: AppContext) { diff --git a/packages/pds/src/app-view/services/feed/index.ts b/packages/pds/src/app-view/services/feed/index.ts index a028bed27f6..a7f153b7a76 100644 --- a/packages/pds/src/app-view/services/feed/index.ts +++ b/packages/pds/src/app-view/services/feed/index.ts @@ -1,5 +1,5 @@ import { sql } from 'kysely' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { dedupeStrs } from '@atproto/common' import { cborToLexRecord } from '@atproto/repo' import Database from '../../../db' diff --git a/packages/pds/src/app-view/services/indexing/index.ts b/packages/pds/src/app-view/services/indexing/index.ts index 0fac4d16190..cdb27344251 100644 --- a/packages/pds/src/app-view/services/indexing/index.ts +++ b/packages/pds/src/app-view/services/indexing/index.ts @@ -1,6 +1,6 @@ import { CID } from 'multiformats/cid' import { WriteOpAction } from '@atproto/repo' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import Database from '../../../db' import * as Post from './plugins/post' import * as Like from './plugins/like' diff --git a/packages/pds/src/app-view/services/indexing/plugins/block.ts b/packages/pds/src/app-view/services/indexing/plugins/block.ts index 79214ad3db2..bb30071c4d1 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/block.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/block.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { CID } from 'multiformats/cid' import * as Block from '../../../../lexicon/types/app/bsky/graph/block' import * as lex from '../../../../lexicon/lexicons' diff --git a/packages/pds/src/app-view/services/indexing/plugins/feed-generator.ts b/packages/pds/src/app-view/services/indexing/plugins/feed-generator.ts index 546dd833b39..c18da08d51c 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/feed-generator.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/feed-generator.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { CID } from 'multiformats/cid' import * as FeedGenerator from '../../../../lexicon/types/app/bsky/feed/generator' import * as lex from '../../../../lexicon/lexicons' diff --git a/packages/pds/src/app-view/services/indexing/plugins/follow.ts b/packages/pds/src/app-view/services/indexing/plugins/follow.ts index 75513bed3c3..c405f288b76 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/follow.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/follow.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { CID } from 'multiformats/cid' import * as Follow from '../../../../lexicon/types/app/bsky/graph/follow' import * as lex from '../../../../lexicon/lexicons' diff --git a/packages/pds/src/app-view/services/indexing/plugins/like.ts b/packages/pds/src/app-view/services/indexing/plugins/like.ts index ce661edc5bb..32d87ebe177 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/like.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/like.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { CID } from 'multiformats/cid' import * as Like from '../../../../lexicon/types/app/bsky/feed/like' import * as lex from '../../../../lexicon/lexicons' diff --git a/packages/pds/src/app-view/services/indexing/plugins/list-item.ts b/packages/pds/src/app-view/services/indexing/plugins/list-item.ts index 812eca3be15..33d0c7e8152 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/list-item.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/list-item.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { CID } from 'multiformats/cid' import * as ListItem from '../../../../lexicon/types/app/bsky/graph/listitem' import * as lex from '../../../../lexicon/lexicons' diff --git a/packages/pds/src/app-view/services/indexing/plugins/list.ts b/packages/pds/src/app-view/services/indexing/plugins/list.ts index b24e83dfe1f..b0250c5be39 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/list.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/list.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { CID } from 'multiformats/cid' import * as List from '../../../../lexicon/types/app/bsky/graph/list' import * as lex from '../../../../lexicon/lexicons' diff --git a/packages/pds/src/app-view/services/indexing/plugins/post.ts b/packages/pds/src/app-view/services/indexing/plugins/post.ts index f3088debac4..37d07539879 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/post.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/post.ts @@ -1,6 +1,6 @@ import { sql } from 'kysely' import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { Record as PostRecord } from '../../../../lexicon/types/app/bsky/feed/post' import { isMain as isEmbedImage } from '../../../../lexicon/types/app/bsky/embed/images' import { isMain as isEmbedExternal } from '../../../../lexicon/types/app/bsky/embed/external' diff --git a/packages/pds/src/app-view/services/indexing/plugins/profile.ts b/packages/pds/src/app-view/services/indexing/plugins/profile.ts index d17d971875f..3b8e6db506c 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/profile.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/profile.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { CID } from 'multiformats/cid' import * as Profile from '../../../../lexicon/types/app/bsky/actor/profile' import * as lex from '../../../../lexicon/lexicons' diff --git a/packages/pds/src/app-view/services/indexing/plugins/repost.ts b/packages/pds/src/app-view/services/indexing/plugins/repost.ts index bd048d27cff..fb05ca57ffd 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/repost.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/repost.ts @@ -1,5 +1,5 @@ import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import * as Repost from '../../../../lexicon/types/app/bsky/feed/repost' import * as lex from '../../../../lexicon/lexicons' import Database from '../../../../db' diff --git a/packages/pds/src/app-view/services/indexing/processor.ts b/packages/pds/src/app-view/services/indexing/processor.ts index 2440c562e0b..8c93fccfb3f 100644 --- a/packages/pds/src/app-view/services/indexing/processor.ts +++ b/packages/pds/src/app-view/services/indexing/processor.ts @@ -1,5 +1,5 @@ import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { cborToLexRecord } from '@atproto/repo' import Database from '../../../db' import DatabaseSchema from '../../../db/database-schema' diff --git a/packages/pds/src/app-view/services/label/index.ts b/packages/pds/src/app-view/services/label/index.ts index 708384fd059..338475ba633 100644 --- a/packages/pds/src/app-view/services/label/index.ts +++ b/packages/pds/src/app-view/services/label/index.ts @@ -1,6 +1,6 @@ import { sql } from 'kysely' import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import Database from '../../../db' import { Label, diff --git a/packages/pds/src/content-reporter/index.ts b/packages/pds/src/content-reporter/index.ts index 659ab5f1e13..0f21fa6986f 100644 --- a/packages/pds/src/content-reporter/index.ts +++ b/packages/pds/src/content-reporter/index.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { RepoRecord } from '@atproto/lexicon' import { CID } from 'multiformats/cid' import * as ui8 from 'uint8arrays' diff --git a/packages/pds/src/event-stream/messages.ts b/packages/pds/src/event-stream/messages.ts index 1fb07dc609b..9c5f00baca6 100644 --- a/packages/pds/src/event-stream/messages.ts +++ b/packages/pds/src/event-stream/messages.ts @@ -1,7 +1,7 @@ // Below specific to message dispatcher import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { WriteOpAction } from '@atproto/repo' export type IndexRecord = { diff --git a/packages/pds/src/feed-gen/index.ts b/packages/pds/src/feed-gen/index.ts index 038c59321b1..46657e87fae 100644 --- a/packages/pds/src/feed-gen/index.ts +++ b/packages/pds/src/feed-gen/index.ts @@ -1,4 +1,4 @@ -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import withFriends from './with-friends' import bskyTeam from './bsky-team' import hotClassic from './hot-classic' diff --git a/packages/pds/src/handle/index.ts b/packages/pds/src/handle/index.ts index 88e1c3fc38f..f9f05c28c48 100644 --- a/packages/pds/src/handle/index.ts +++ b/packages/pds/src/handle/index.ts @@ -1,4 +1,4 @@ -import * as ident from '@atproto/identifier' +import * as ident from '@atproto/syntax' import { InvalidRequestError } from '@atproto/xrpc-server' import { reservedSubdomains } from './reserved' import { hasExplicitSlur } from '../content-reporter/explicit-slurs' diff --git a/packages/pds/src/labeler/base.ts b/packages/pds/src/labeler/base.ts index 23479e9ff6f..8a52ed2c717 100644 --- a/packages/pds/src/labeler/base.ts +++ b/packages/pds/src/labeler/base.ts @@ -1,7 +1,7 @@ import Database from '../db' import { BlobStore, cidForRecord } from '@atproto/repo' import { dedupe, getFieldsFromRecord } from './util' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { labelerLogger as log } from '../logger' import { BackgroundQueue } from '../event-stream/background-queue' import { CID } from 'multiformats/cid' diff --git a/packages/pds/src/repo/prepare.ts b/packages/pds/src/repo/prepare.ts index 518844f76f7..c93030d1e6f 100644 --- a/packages/pds/src/repo/prepare.ts +++ b/packages/pds/src/repo/prepare.ts @@ -1,5 +1,5 @@ import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { TID, dataToCborBlock } from '@atproto/common' import { LexiconDefNotFoundError, diff --git a/packages/pds/src/repo/types.ts b/packages/pds/src/repo/types.ts index 611f459da0a..a66d3aa0f65 100644 --- a/packages/pds/src/repo/types.ts +++ b/packages/pds/src/repo/types.ts @@ -1,5 +1,5 @@ import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { WriteOpAction } from '@atproto/repo' import { RepoRecord } from '@atproto/lexicon' diff --git a/packages/pds/src/services/local/index.ts b/packages/pds/src/services/local/index.ts index c697d55d010..a850a46c55e 100644 --- a/packages/pds/src/services/local/index.ts +++ b/packages/pds/src/services/local/index.ts @@ -1,6 +1,6 @@ import util from 'util' import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { cborToLexRecord } from '@atproto/repo' import Database from '../../db' import { Record as PostRecord } from '../../lexicon/types/app/bsky/feed/post' diff --git a/packages/pds/src/services/moderation/index.ts b/packages/pds/src/services/moderation/index.ts index 92ffdcc9d33..2f0bcb5415d 100644 --- a/packages/pds/src/services/moderation/index.ts +++ b/packages/pds/src/services/moderation/index.ts @@ -1,7 +1,7 @@ import { Selectable, sql } from 'kysely' import { CID } from 'multiformats/cid' import { BlobStore } from '@atproto/repo' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { InvalidRequestError } from '@atproto/xrpc-server' import Database from '../../db' import { MessageQueue } from '../../event-stream/types' diff --git a/packages/pds/src/services/moderation/views.ts b/packages/pds/src/services/moderation/views.ts index 8f0400cfab4..5676e086f35 100644 --- a/packages/pds/src/services/moderation/views.ts +++ b/packages/pds/src/services/moderation/views.ts @@ -1,6 +1,6 @@ import { Selectable } from 'kysely' import { ArrayEl, cborBytesToRecord } from '@atproto/common' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import Database from '../../db' import { MessageQueue } from '../../event-stream/types' import { DidHandle } from '../../db/tables/did-handle' diff --git a/packages/pds/src/services/record/index.ts b/packages/pds/src/services/record/index.ts index 604a6e7b71c..857ae7f1d95 100644 --- a/packages/pds/src/services/record/index.ts +++ b/packages/pds/src/services/record/index.ts @@ -1,6 +1,6 @@ import { CID } from 'multiformats/cid' -import { AtUri, ensureValidAtUri } from '@atproto/uri' -import * as ident from '@atproto/identifier' +import { AtUri, ensureValidAtUri } from '@atproto/syntax' +import * as ident from '@atproto/syntax' import { cborToLexRecord, WriteOpAction } from '@atproto/repo' import { dbLogger as log } from '../../logger' import Database from '../../db' diff --git a/packages/pds/src/services/repo/blobs.ts b/packages/pds/src/services/repo/blobs.ts index d0e92cd0298..5f4f757f768 100644 --- a/packages/pds/src/services/repo/blobs.ts +++ b/packages/pds/src/services/repo/blobs.ts @@ -4,7 +4,7 @@ import { CID } from 'multiformats/cid' import bytes from 'bytes' import { fromStream as fileTypeFromStream } from 'file-type' import { BlobStore, CidSet, WriteOpAction } from '@atproto/repo' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { cloneStream, sha256RawToCid, streamSize } from '@atproto/common' import { InvalidRequestError } from '@atproto/xrpc-server' import { PreparedBlobRef, PreparedWrite } from '../../repo/types' diff --git a/packages/pds/src/services/repo/index.ts b/packages/pds/src/services/repo/index.ts index 50d60872a8a..aed7a862a39 100644 --- a/packages/pds/src/services/repo/index.ts +++ b/packages/pds/src/services/repo/index.ts @@ -8,7 +8,7 @@ import { WriteOpAction, } from '@atproto/repo' import * as repo from '@atproto/repo' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { InvalidRequestError } from '@atproto/xrpc-server' import Database from '../../db' import { MessageQueue } from '../../event-stream/types' diff --git a/packages/pds/tests/_util.ts b/packages/pds/tests/_util.ts index c1297a50967..0b94f09148b 100644 --- a/packages/pds/tests/_util.ts +++ b/packages/pds/tests/_util.ts @@ -4,7 +4,7 @@ import path from 'path' import * as crypto from '@atproto/crypto' import * as plc from '@did-plc/lib' import { PlcServer, Database as PlcDatabase } from '@did-plc/server' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { randomStr } from '@atproto/crypto' import { uniqueLockId } from '@atproto/dev-env' import { CID } from 'multiformats/cid' diff --git a/packages/pds/tests/crud.test.ts b/packages/pds/tests/crud.test.ts index 00f7c778009..12128bef898 100644 --- a/packages/pds/tests/crud.test.ts +++ b/packages/pds/tests/crud.test.ts @@ -1,5 +1,5 @@ import fs from 'fs/promises' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import AtpAgent from '@atproto/api' import * as createRecord from '@atproto/api/src/client/types/com/atproto/repo/createRecord' import * as putRecord from '@atproto/api/src/client/types/com/atproto/repo/putRecord' diff --git a/packages/pds/tests/duplicate-records.test.ts b/packages/pds/tests/duplicate-records.test.ts index 2815c0bcb36..b435dca32f3 100644 --- a/packages/pds/tests/duplicate-records.test.ts +++ b/packages/pds/tests/duplicate-records.test.ts @@ -1,5 +1,5 @@ import { CID } from 'multiformats/cid' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { cidForCbor, TID, cborEncode } from '@atproto/common' import { CloseFn, runTestServer } from './_util' import { Database } from '../src' diff --git a/packages/pds/tests/handle-validation.test.ts b/packages/pds/tests/handle-validation.test.ts index 14b9b2d2646..bf55d61e49f 100644 --- a/packages/pds/tests/handle-validation.test.ts +++ b/packages/pds/tests/handle-validation.test.ts @@ -1,4 +1,4 @@ -import { isValidTld } from '@atproto/identifier' +import { isValidTld } from '@atproto/syntax' import { ensureHandleServiceConstraints } from '../src/handle' import { UnacceptableWordValidator } from '../src/content-reporter/validator' diff --git a/packages/pds/tests/indexing.test.ts b/packages/pds/tests/indexing.test.ts index 8529bec0ffa..a99053a07c7 100644 --- a/packages/pds/tests/indexing.test.ts +++ b/packages/pds/tests/indexing.test.ts @@ -4,7 +4,7 @@ import AtpAgent, { AppBskyFeedLike, AppBskyFeedRepost, } from '@atproto/api' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { CloseFn, forSnapshot, runTestServer, TestServerInfo } from './_util' import { SeedClient } from './seeds/client' import usersSeed from './seeds/users' diff --git a/packages/pds/tests/migrations/blob-creator.test.ts b/packages/pds/tests/migrations/blob-creator.test.ts index defe3bde6f4..bc9cc72faf4 100644 --- a/packages/pds/tests/migrations/blob-creator.test.ts +++ b/packages/pds/tests/migrations/blob-creator.test.ts @@ -2,7 +2,7 @@ import { Database } from '../../src' import { randomStr } from '@atproto/crypto' import { cidForCbor, TID } from '@atproto/common' import { Kysely } from 'kysely' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' describe.skip('blob creator migration', () => { let db: Database diff --git a/packages/pds/tests/migrations/indexed-at-on-record.test.ts b/packages/pds/tests/migrations/indexed-at-on-record.test.ts index 02147816203..664ae3e16cd 100644 --- a/packages/pds/tests/migrations/indexed-at-on-record.test.ts +++ b/packages/pds/tests/migrations/indexed-at-on-record.test.ts @@ -1,7 +1,7 @@ import { Database } from '../../src' import { randomStr } from '@atproto/crypto' import { dataToCborBlock, TID } from '@atproto/common' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { Kysely } from 'kysely' describe.skip('indexedAt on record migration', () => { diff --git a/packages/pds/tests/moderation.test.ts b/packages/pds/tests/moderation.test.ts index f47eb17a81e..0861d852aca 100644 --- a/packages/pds/tests/moderation.test.ts +++ b/packages/pds/tests/moderation.test.ts @@ -1,5 +1,5 @@ import AtpAgent, { ComAtprotoAdminTakeModerationAction } from '@atproto/api' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { BlobNotFoundError } from '@atproto/repo' import { adminAuth, diff --git a/packages/pds/tests/seeds/client.ts b/packages/pds/tests/seeds/client.ts index 59735d865f5..2918de9c7dd 100644 --- a/packages/pds/tests/seeds/client.ts +++ b/packages/pds/tests/seeds/client.ts @@ -7,7 +7,7 @@ import { InputSchema as CreateReportInput } from '@atproto/api/src/client/types/ import { Record as PostRecord } from '@atproto/api/src/client/types/app/bsky/feed/post' import { Record as LikeRecord } from '@atproto/api/src/client/types/app/bsky/feed/like' import { Record as FollowRecord } from '@atproto/api/src/client/types/app/bsky/graph/follow' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { BlobRef } from '@atproto/lexicon' import { adminAuth } from '../_util' import { ids } from '../../src/lexicon/lexicons' diff --git a/packages/pds/tests/sync/sync.test.ts b/packages/pds/tests/sync/sync.test.ts index 6d3814a78be..9605d285a4c 100644 --- a/packages/pds/tests/sync/sync.test.ts +++ b/packages/pds/tests/sync/sync.test.ts @@ -4,7 +4,7 @@ import { cidForCbor, TID } from '@atproto/common' import { randomStr } from '@atproto/crypto' import * as repo from '@atproto/repo' import { collapseWriteLog, MemoryBlockstore, readCar } from '@atproto/repo' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { CID } from 'multiformats/cid' import { AppContext } from '../../src' diff --git a/packages/pds/tests/views/admin/get-record.test.ts b/packages/pds/tests/views/admin/get-record.test.ts index 34933973caf..797a4b726f2 100644 --- a/packages/pds/tests/views/admin/get-record.test.ts +++ b/packages/pds/tests/views/admin/get-record.test.ts @@ -1,5 +1,5 @@ import AtpAgent from '@atproto/api' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import { ACKNOWLEDGE, TAKEDOWN, diff --git a/packages/repo/package.json b/packages/repo/package.json index 3f6ca791a77..ffda8e60550 100644 --- a/packages/repo/package.json +++ b/packages/repo/package.json @@ -29,9 +29,9 @@ "dependencies": { "@atproto/common": "*", "@atproto/crypto": "*", + "@atproto/syntax": "*", "@atproto/identity": "*", "@atproto/lexicon": "*", - "@atproto/nsid": "*", "@ipld/car": "^3.2.3", "@ipld/dag-cbor": "^7.0.0", "multiformats": "^9.6.4", diff --git a/packages/syntax/README.md b/packages/syntax/README.md new file mode 100644 index 00000000000..e9c5d151609 --- /dev/null +++ b/packages/syntax/README.md @@ -0,0 +1,61 @@ +# Syntax + +Validation logic for AT identifiers - DIDs, Handles, NSIDs, and AT URIs + +## Usage + +```typescript +import * as identifier from '@atproto/syntax' + +isValidHandle('alice.test') // returns true +ensureValidHandle('alice.test') // returns void + +isValidHandle('al!ce.test') // returns false +ensureValidHandle('al!ce.test') // throws + +ensureValidDid('did:method:val') // returns void +ensureValidDid(':did:method:val') // throws +``` + +## NameSpaced IDs (NSID) + +```typescript +import { NSID } from '@atproto/syntax' + +const id1 = NSID.parse('com.example.foo') +id1.authority // => 'example.com' +id1.name // => 'foo' +id1.toString() // => 'com.example.foo' + +const id2 = NSID.create('example.com', 'foo') +id2.authority // => 'example.com' +id2.name // => 'foo' +id2.toString() // => 'com.example.foo' + +const id3 = NSID.create('example.com', 'someRecord') +id3.authority // => 'example.com' +id3.name // => 'someRecord' +id3.toString() // => 'com.example.someRecord' + +NSID.isValid('com.example.foo') // => true +NSID.isValid('com.example.someRecord') // => true +NSID.isValid('example.com/foo') // => false +NSID.isValid('foo') // => false +``` + +## AT URI + +```typescript +import { AtUri } from '@atproto/syntax' + +const uri = new AtUri('at://bob.com/com.example.post/1234') +uri.protocol // => 'at:' +uri.origin // => 'at://bob.com' +uri.hostname // => 'bob.com' +uri.collection // => 'com.example.post' +uri.rkey // => '1234' +``` + +## License + +MIT diff --git a/packages/syntax/babel.config.js b/packages/syntax/babel.config.js new file mode 100644 index 00000000000..0126e9dbaa6 --- /dev/null +++ b/packages/syntax/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js') diff --git a/packages/syntax/build.js b/packages/syntax/build.js new file mode 100644 index 00000000000..5628aa4f4eb --- /dev/null +++ b/packages/syntax/build.js @@ -0,0 +1,22 @@ +const pkgJson = require('@npmcli/package-json') +const { nodeExternalsPlugin } = require('esbuild-node-externals') + +const buildShallow = + process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' + +if (process.argv.includes('--update-main-to-dist')) { + return pkgJson + .load(__dirname) + .then((pkg) => pkg.update({ main: 'dist/index.js' })) + .then((pkg) => pkg.save()) +} + +require('esbuild').build({ + logLevel: 'info', + entryPoints: ['src/index.ts'], + bundle: true, + sourcemap: true, + outdir: 'dist', + platform: 'node', + plugins: buildShallow ? [nodeExternalsPlugin()] : [], +}) diff --git a/packages/syntax/jest.config.js b/packages/syntax/jest.config.js new file mode 100644 index 00000000000..096d01562c4 --- /dev/null +++ b/packages/syntax/jest.config.js @@ -0,0 +1,6 @@ +const base = require('../../jest.config.base.js') + +module.exports = { + ...base, + displayName: 'Identifier', +} diff --git a/packages/syntax/package.json b/packages/syntax/package.json new file mode 100644 index 00000000000..9a257b9bb4a --- /dev/null +++ b/packages/syntax/package.json @@ -0,0 +1,32 @@ +{ + "name": "@atproto/syntax", + "version": "0.1.0", + "main": "src/index.ts", + "scripts": { + "test": "jest", + "prettier": "prettier --check src/ tests/", + "prettier:fix": "prettier --write src/ tests/", + "lint": "eslint . --ext .ts,.tsx", + "lint:fix": "yarn lint --fix", + "verify": "run-p prettier lint", + "verify:fix": "yarn prettier:fix && yarn lint:fix", + "build": "node ./build.js", + "postbuild": "tsc --build tsconfig.build.json", + "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", + "update-main-to-src": "node ./update-pkg.js --update-main-to-src", + "prepublish": "npm run update-main-to-dist", + "postpublish": "npm run update-main-to-src" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/bluesky-social/atproto.git", + "directory": "packages/syntax" + }, + "dependencies": { + "@atproto/common-web": "*" + }, + "browser": { + "dns/promises": false + } +} diff --git a/packages/syntax/src/aturi.ts b/packages/syntax/src/aturi.ts new file mode 100644 index 00000000000..516d3fc61ab --- /dev/null +++ b/packages/syntax/src/aturi.ts @@ -0,0 +1,136 @@ +export * from './aturi_validation' + +export const ATP_URI_REGEX = + // proto- --did-------------- --name---------------- --path---- --query-- --hash-- + /^(at:\/\/)?((?:did:[a-z0-9:%-]+)|(?:[a-z0-9][a-z0-9.:-]*))(\/[^?#\s]*)?(\?[^#\s]+)?(#[^\s]+)?$/i +// --path----- --query-- --hash-- +const RELATIVE_REGEX = /^(\/[^?#\s]*)?(\?[^#\s]+)?(#[^\s]+)?$/i + +export class AtUri { + hash: string + host: string + pathname: string + searchParams: URLSearchParams + + constructor(uri: string, base?: string) { + let parsed + if (base) { + parsed = parse(base) + if (!parsed) { + throw new Error(`Invalid at uri: ${base}`) + } + const relativep = parseRelative(uri) + if (!relativep) { + throw new Error(`Invalid path: ${uri}`) + } + Object.assign(parsed, relativep) + } else { + parsed = parse(uri) + if (!parsed) { + throw new Error(`Invalid at uri: ${uri}`) + } + } + + this.hash = parsed.hash + this.host = parsed.host + this.pathname = parsed.pathname + this.searchParams = parsed.searchParams + } + + static make(handleOrDid: string, collection?: string, rkey?: string) { + let str = handleOrDid + if (collection) str += '/' + collection + if (rkey) str += '/' + rkey + return new AtUri(str) + } + + get protocol() { + return 'at:' + } + + get origin() { + return `at://${this.host}` + } + + get hostname() { + return this.host + } + + set hostname(v: string) { + this.host = v + } + + get search() { + return this.searchParams.toString() + } + + set search(v: string) { + this.searchParams = new URLSearchParams(v) + } + + get collection() { + return this.pathname.split('/').filter(Boolean)[0] || '' + } + + set collection(v: string) { + const parts = this.pathname.split('/').filter(Boolean) + parts[0] = v + this.pathname = parts.join('/') + } + + get rkey() { + return this.pathname.split('/').filter(Boolean)[1] || '' + } + + set rkey(v: string) { + const parts = this.pathname.split('/').filter(Boolean) + if (!parts[0]) parts[0] = 'undefined' + parts[1] = v + this.pathname = parts.join('/') + } + + get href() { + return this.toString() + } + + toString() { + let path = this.pathname || '/' + if (!path.startsWith('/')) { + path = `/${path}` + } + let qs = this.searchParams.toString() + if (qs && !qs.startsWith('?')) { + qs = `?${qs}` + } + let hash = this.hash + if (hash && !hash.startsWith('#')) { + hash = `#${hash}` + } + return `at://${this.host}${path}${qs}${hash}` + } +} + +function parse(str: string) { + const match = ATP_URI_REGEX.exec(str) + if (match) { + return { + hash: match[5] || '', + host: match[2] || '', + pathname: match[3] || '', + searchParams: new URLSearchParams(match[4] || ''), + } + } + return undefined +} + +function parseRelative(str: string) { + const match = RELATIVE_REGEX.exec(str) + if (match) { + return { + hash: match[3] || '', + pathname: match[1] || '', + searchParams: new URLSearchParams(match[2] || ''), + } + } + return undefined +} diff --git a/packages/uri/src/validation.ts b/packages/syntax/src/aturi_validation.ts similarity index 90% rename from packages/uri/src/validation.ts rename to packages/syntax/src/aturi_validation.ts index af0c359ca6e..a272b15a082 100644 --- a/packages/uri/src/validation.ts +++ b/packages/syntax/src/aturi_validation.ts @@ -1,5 +1,6 @@ -import * as id from '@atproto/identifier' -import * as nsid from '@atproto/nsid' +import { ensureValidHandle, ensureValidHandleRegex } from './handle' +import { ensureValidDid, ensureValidDidRegex } from './did' +import { ensureValidNsid, ensureValidNsidRegex } from './nsid' // Human-readable constraints on ATURI: // - following regular URLs, a 8KByte hard total length limit @@ -37,10 +38,10 @@ export const ensureValidAtUri = (uri: string) => { } try { - id.ensureValidHandle(parts[2]) + ensureValidHandle(parts[2]) } catch { try { - id.ensureValidDid(parts[2]) + ensureValidDid(parts[2]) } catch { throw new Error('ATURI authority must be a valid handle or DID') } @@ -53,7 +54,7 @@ export const ensureValidAtUri = (uri: string) => { ) } try { - nsid.ensureValidNsid(parts[3]) + ensureValidNsid(parts[3]) } catch { throw new Error( 'ATURI requires first path segment (if supplied) to be valid NSID', @@ -107,10 +108,10 @@ export const ensureValidAtUriRegex = (uri: string): void => { const groups = rm.groups try { - id.ensureValidHandleRegex(groups.authority) + ensureValidHandleRegex(groups.authority) } catch { try { - id.ensureValidDidRegex(groups.authority) + ensureValidDidRegex(groups.authority) } catch { throw new Error('ATURI authority must be a valid handle or DID') } @@ -118,7 +119,7 @@ export const ensureValidAtUriRegex = (uri: string): void => { if (groups.collection) { try { - nsid.ensureValidNsidRegex(groups.collection) + ensureValidNsidRegex(groups.collection) } catch { throw new Error('ATURI collection path segment must be a valid NSID') } diff --git a/packages/identifier/src/did.ts b/packages/syntax/src/did.ts similarity index 100% rename from packages/identifier/src/did.ts rename to packages/syntax/src/did.ts diff --git a/packages/identifier/src/handle.ts b/packages/syntax/src/handle.ts similarity index 100% rename from packages/identifier/src/handle.ts rename to packages/syntax/src/handle.ts diff --git a/packages/syntax/src/index.ts b/packages/syntax/src/index.ts new file mode 100644 index 00000000000..0b056b995ae --- /dev/null +++ b/packages/syntax/src/index.ts @@ -0,0 +1,4 @@ +export * from './handle' +export * from './did' +export * from './nsid' +export * from './aturi' diff --git a/packages/syntax/src/nsid.ts b/packages/syntax/src/nsid.ts new file mode 100644 index 00000000000..b436912bf7d --- /dev/null +++ b/packages/syntax/src/nsid.ts @@ -0,0 +1,111 @@ +/* +Grammar: + +alpha = "a" / "b" / "c" / "d" / "e" / "f" / "g" / "h" / "i" / "j" / "k" / "l" / "m" / "n" / "o" / "p" / "q" / "r" / "s" / "t" / "u" / "v" / "w" / "x" / "y" / "z" / "A" / "B" / "C" / "D" / "E" / "F" / "G" / "H" / "I" / "J" / "K" / "L" / "M" / "N" / "O" / "P" / "Q" / "R" / "S" / "T" / "U" / "V" / "W" / "X" / "Y" / "Z" +number = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" / "0" +delim = "." +segment = alpha *( alpha / number / "-" ) +authority = segment *( delim segment ) +name = alpha *( alpha ) +nsid = authority delim name + +*/ + +export class NSID { + segments: string[] = [] + + static parse(nsid: string): NSID { + return new NSID(nsid) + } + + static create(authority: string, name: string): NSID { + const segments = [...authority.split('.').reverse(), name].join('.') + return new NSID(segments) + } + + static isValid(nsid: string): boolean { + try { + NSID.parse(nsid) + return true + } catch (e) { + return false + } + } + + constructor(nsid: string) { + ensureValidNsid(nsid) + this.segments = nsid.split('.') + } + + get authority() { + return this.segments + .slice(0, this.segments.length - 1) + .reverse() + .join('.') + } + + get name() { + return this.segments.at(this.segments.length - 1) + } + + toString() { + return this.segments.join('.') + } +} + +// Human readable constraints on NSID: +// - a valid domain in reversed notation +// - followed by an additional period-separated name, which is camel-case letters +export const ensureValidNsid = (nsid: string): void => { + const toCheck = nsid + + // check that all chars are boring ASCII + if (!/^[a-zA-Z0-9.-]*$/.test(toCheck)) { + throw new InvalidNsidError( + 'Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)', + ) + } + + if (toCheck.length > 253 + 1 + 63) { + throw new InvalidNsidError('NSID is too long (317 chars max)') + } + const labels = toCheck.split('.') + if (labels.length < 3) { + throw new InvalidNsidError('NSID needs at least three parts') + } + for (let i = 0; i < labels.length; i++) { + const l = labels[i] + if (l.length < 1) { + throw new InvalidNsidError('NSID parts can not be empty') + } + if (l.length > 63) { + throw new InvalidNsidError('NSID part too long (max 63 chars)') + } + if (l.endsWith('-') || l.startsWith('-')) { + throw new InvalidNsidError('NSID parts can not start or end with hyphen') + } + if (/^[0-9]/.test(l) && i == 0) { + throw new InvalidNsidError('NSID first part may not start with a digit') + } + if (!/^[a-zA-Z]+$/.test(l) && i + 1 == labels.length) { + throw new InvalidNsidError('NSID name part must be only letters') + } + } +} + +export const ensureValidNsidRegex = (nsid: string): void => { + // simple regex to enforce most constraints via just regex and length. + // hand wrote this regex based on above constraints + if ( + !/^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z]{0,61}[a-zA-Z])?)$/.test( + nsid, + ) + ) { + throw new InvalidNsidError("NSID didn't validate via regex") + } + if (nsid.length > 253 + 1 + 63) { + throw new InvalidNsidError('NSID is too long (317 chars max)') + } +} + +export class InvalidNsidError extends Error {} diff --git a/packages/uri/tests/uri.test.ts b/packages/syntax/tests/aturi.test.ts similarity index 100% rename from packages/uri/tests/uri.test.ts rename to packages/syntax/tests/aturi.test.ts diff --git a/packages/identifier/tests/did.test.ts b/packages/syntax/tests/did.test.ts similarity index 100% rename from packages/identifier/tests/did.test.ts rename to packages/syntax/tests/did.test.ts diff --git a/packages/identifier/tests/handle.test.ts b/packages/syntax/tests/handle.test.ts similarity index 100% rename from packages/identifier/tests/handle.test.ts rename to packages/syntax/tests/handle.test.ts diff --git a/packages/nsid/tests/nsid.test.ts b/packages/syntax/tests/nsid.test.ts similarity index 100% rename from packages/nsid/tests/nsid.test.ts rename to packages/syntax/tests/nsid.test.ts diff --git a/packages/syntax/tsconfig.build.json b/packages/syntax/tsconfig.build.json new file mode 100644 index 00000000000..27df65b89e2 --- /dev/null +++ b/packages/syntax/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["**/*.spec.ts", "**/*.test.ts"] +} \ No newline at end of file diff --git a/packages/syntax/tsconfig.json b/packages/syntax/tsconfig.json new file mode 100644 index 00000000000..aaa3b2d88bf --- /dev/null +++ b/packages/syntax/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", // Your outDir, + "emitDeclarationOnly": true + }, + "include": ["./src","__tests__/**/**.ts"], + "references": [ + { "path": "../common/tsconfig.build.json" }, + ] +} \ No newline at end of file diff --git a/packages/syntax/update-pkg.js b/packages/syntax/update-pkg.js new file mode 100644 index 00000000000..15d70bdab33 --- /dev/null +++ b/packages/syntax/update-pkg.js @@ -0,0 +1,14 @@ +const pkgJson = require('@npmcli/package-json') + +if (process.argv.includes('--update-main-to-dist')) { + return pkgJson + .load(__dirname) + .then((pkg) => pkg.update({ main: 'dist/index.js' })) + .then((pkg) => pkg.save()) +} +if (process.argv.includes('--update-main-to-src')) { + return pkgJson + .load(__dirname) + .then((pkg) => pkg.update({ main: 'src/index.ts' })) + .then((pkg) => pkg.save()) +} diff --git a/packages/uri/package.json b/packages/uri/package.json index 5f3af4aa9a8..82e96c1a0ae 100644 --- a/packages/uri/package.json +++ b/packages/uri/package.json @@ -3,9 +3,9 @@ "version": "0.1.0", "main": "src/index.ts", "scripts": { - "test": "jest", - "prettier": "prettier --check src/ tests/", - "prettier:fix": "prettier --write src/ tests/", + "test": "true", + "prettier": "prettier --check src/", + "prettier:fix": "prettier --write src/", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "yarn lint --fix", "verify": "run-p prettier lint", @@ -24,7 +24,6 @@ "directory": "packages/uri" }, "dependencies": { - "@atproto/identifier": "*", - "@atproto/nsid": "*" + "@atproto/syntax": "*" } } diff --git a/packages/uri/src/index.ts b/packages/uri/src/index.ts index 538b1ad736f..1c657b16049 100644 --- a/packages/uri/src/index.ts +++ b/packages/uri/src/index.ts @@ -1,136 +1,6 @@ -export * from './validation' - -export const ATP_URI_REGEX = - // proto- --did-------------- --name---------------- --path---- --query-- --hash-- - /^(at:\/\/)?((?:did:[a-z0-9:%-]+)|(?:[a-z0-9][a-z0-9.:-]*))(\/[^?#\s]*)?(\?[^#\s]+)?(#[^\s]+)?$/i -// --path----- --query-- --hash-- -const RELATIVE_REGEX = /^(\/[^?#\s]*)?(\?[^#\s]+)?(#[^\s]+)?$/i - -export class AtUri { - hash: string - host: string - pathname: string - searchParams: URLSearchParams - - constructor(uri: string, base?: string) { - let parsed - if (base) { - parsed = parse(base) - if (!parsed) { - throw new Error(`Invalid at uri: ${base}`) - } - const relativep = parseRelative(uri) - if (!relativep) { - throw new Error(`Invalid path: ${uri}`) - } - Object.assign(parsed, relativep) - } else { - parsed = parse(uri) - if (!parsed) { - throw new Error(`Invalid at uri: ${uri}`) - } - } - - this.hash = parsed.hash - this.host = parsed.host - this.pathname = parsed.pathname - this.searchParams = parsed.searchParams - } - - static make(handleOrDid: string, collection?: string, rkey?: string) { - let str = handleOrDid - if (collection) str += '/' + collection - if (rkey) str += '/' + rkey - return new AtUri(str) - } - - get protocol() { - return 'at:' - } - - get origin() { - return `at://${this.host}` - } - - get hostname() { - return this.host - } - - set hostname(v: string) { - this.host = v - } - - get search() { - return this.searchParams.toString() - } - - set search(v: string) { - this.searchParams = new URLSearchParams(v) - } - - get collection() { - return this.pathname.split('/').filter(Boolean)[0] || '' - } - - set collection(v: string) { - const parts = this.pathname.split('/').filter(Boolean) - parts[0] = v - this.pathname = parts.join('/') - } - - get rkey() { - return this.pathname.split('/').filter(Boolean)[1] || '' - } - - set rkey(v: string) { - const parts = this.pathname.split('/').filter(Boolean) - if (!parts[0]) parts[0] = 'undefined' - parts[1] = v - this.pathname = parts.join('/') - } - - get href() { - return this.toString() - } - - toString() { - let path = this.pathname || '/' - if (!path.startsWith('/')) { - path = `/${path}` - } - let qs = this.searchParams.toString() - if (qs && !qs.startsWith('?')) { - qs = `?${qs}` - } - let hash = this.hash - if (hash && !hash.startsWith('#')) { - hash = `#${hash}` - } - return `at://${this.host}${path}${qs}${hash}` - } -} - -function parse(str: string) { - const match = ATP_URI_REGEX.exec(str) - if (match) { - return { - hash: match[5] || '', - host: match[2] || '', - pathname: match[3] || '', - searchParams: new URLSearchParams(match[4] || ''), - } - } - return undefined -} - -function parseRelative(str: string) { - const match = RELATIVE_REGEX.exec(str) - if (match) { - return { - hash: match[3] || '', - pathname: match[1] || '', - searchParams: new URLSearchParams(match[2] || ''), - } - } - return undefined -} +export { + ATP_URI_REGEX, + AtUri, + ensureValidAtUri, + ensureValidAtUriRegex, +} from '@atproto/syntax' diff --git a/tsconfig.json b/tsconfig.json index efc5c955572..c45350986b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,6 +34,7 @@ { "path": "./packages/dev-env" }, { "path": "./packages/identity/tsconfig.build.json" }, { "path": "./packages/identifier/tsconfig.build.json" }, + { "path": "./packages/syntax/tsconfig.build.json" }, { "path": "./packages/lexicon/tsconfig.build.json" }, { "path": "./packages/lex-cli/tsconfig.build.json" }, { "path": "./packages/nsid/tsconfig.build.json" }, From c2f47b725d1627738b3960aba7789162b1f0544a Mon Sep 17 00:00:00 2001 From: bnewbold Date: Fri, 25 Aug 2023 08:19:27 -0700 Subject: [PATCH 193/237] identity: parse support for newer DID document format (Multikey) (#1475) * identity: parse support for newer DID document format (Multikey) * identity: lint fixes * identity: DID doc parsing allow full DID URL in service 'id' * add parse/format multikey methods --------- Co-authored-by: dholms --- packages/crypto/src/const.ts | 3 +- packages/crypto/src/did.ts | 37 +++++-- packages/identity/src/did/atproto-data.ts | 13 ++- packages/identity/tests/did-document.test.ts | 103 +++++++++++++++++++ 4 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 packages/identity/tests/did-document.test.ts diff --git a/packages/crypto/src/const.ts b/packages/crypto/src/const.ts index 6f09574ac8a..7fc1e0fcf43 100644 --- a/packages/crypto/src/const.ts +++ b/packages/crypto/src/const.ts @@ -1,6 +1,7 @@ export const P256_DID_PREFIX = new Uint8Array([0x80, 0x24]) export const SECP256K1_DID_PREFIX = new Uint8Array([0xe7, 0x01]) -export const BASE58_DID_PREFIX = 'did:key:z' // z is the multibase prefix for base58btc byte encoding +export const BASE58_MULTIBASE_PREFIX = 'z' +export const DID_KEY_PREFIX = 'did:key:' export const P256_JWT_ALG = 'ES256' export const SECP256K1_JWT_ALG = 'ES256K' diff --git a/packages/crypto/src/did.ts b/packages/crypto/src/did.ts index d95abdc734e..7ced78a394d 100644 --- a/packages/crypto/src/did.ts +++ b/packages/crypto/src/did.ts @@ -2,19 +2,24 @@ import * as uint8arrays from 'uint8arrays' import * as p256 from './p256/encoding' import * as secp from './secp256k1/encoding' import plugins from './plugins' -import { P256_JWT_ALG, SECP256K1_JWT_ALG, BASE58_DID_PREFIX } from './const' +import { + BASE58_MULTIBASE_PREFIX, + DID_KEY_PREFIX, + P256_JWT_ALG, + SECP256K1_JWT_ALG, +} from './const' -export type ParsedDidKey = { +export type ParsedMultikey = { jwtAlg: string keyBytes: Uint8Array } -export const parseDidKey = (did: string): ParsedDidKey => { - if (!did.startsWith(BASE58_DID_PREFIX)) { - throw new Error(`Incorrect prefix for did:key: ${did}`) +export const parseMultikey = (multikey: string): ParsedMultikey => { + if (!multikey.startsWith(BASE58_MULTIBASE_PREFIX)) { + throw new Error(`Incorrect prefix for multikey: ${multikey}`) } const prefixedBytes = uint8arrays.fromString( - did.slice(BASE58_DID_PREFIX.length), + multikey.slice(BASE58_MULTIBASE_PREFIX.length), 'base58btc', ) const plugin = plugins.find((p) => hasPrefix(prefixedBytes, p.prefix)) @@ -33,7 +38,10 @@ export const parseDidKey = (did: string): ParsedDidKey => { } } -export const formatDidKey = (jwtAlg: string, keyBytes: Uint8Array): string => { +export const formatMultikey = ( + jwtAlg: string, + keyBytes: Uint8Array, +): string => { const plugin = plugins.find((p) => p.jwtAlg === jwtAlg) if (!plugin) { throw new Error('Unsupported key type') @@ -44,7 +52,20 @@ export const formatDidKey = (jwtAlg: string, keyBytes: Uint8Array): string => { keyBytes = secp.compressPubkey(keyBytes) } const prefixedBytes = uint8arrays.concat([plugin.prefix, keyBytes]) - return BASE58_DID_PREFIX + uint8arrays.toString(prefixedBytes, 'base58btc') + return ( + BASE58_MULTIBASE_PREFIX + uint8arrays.toString(prefixedBytes, 'base58btc') + ) +} + +export const parseDidKey = (did: string): ParsedMultikey => { + if (!did.startsWith(DID_KEY_PREFIX)) { + throw new Error(`Incorrect prefix for did:key: ${did}`) + } + return parseMultikey(did.slice(DID_KEY_PREFIX.length)) +} + +export const formatDidKey = (jwtAlg: string, keyBytes: Uint8Array): string => { + return DID_KEY_PREFIX + formatMultikey(jwtAlg, keyBytes) } const hasPrefix = (bytes: Uint8Array, prefix: Uint8Array): boolean => { diff --git a/packages/identity/src/did/atproto-data.ts b/packages/identity/src/did/atproto-data.ts index ae958a784cc..3e7ee5829eb 100644 --- a/packages/identity/src/did/atproto-data.ts +++ b/packages/identity/src/did/atproto-data.ts @@ -10,13 +10,16 @@ export const getDid = (doc: DidDocument): string => { } export const getKey = (doc: DidDocument): string | undefined => { + const did = getDid(doc) let keys = doc.verificationMethod if (!keys) return undefined if (typeof keys !== 'object') return undefined if (!Array.isArray(keys)) { keys = [keys] } - const found = keys.find((key) => key.id === '#atproto') + const found = keys.find( + (key) => key.id === '#atproto' || key.id === `${did}#atproto`, + ) if (!found) return undefined // @TODO support jwk @@ -28,6 +31,9 @@ export const getKey = (doc: DidDocument): string | undefined => { didKey = crypto.formatDidKey(crypto.P256_JWT_ALG, keyBytes) } else if (found.type === 'EcdsaSecp256k1VerificationKey2019') { didKey = crypto.formatDidKey(crypto.SECP256K1_JWT_ALG, keyBytes) + } else if (found.type === 'Multikey') { + const parsed = crypto.parseMultikey(found.publicKeyMultibase) + didKey = crypto.formatDidKey(parsed.jwtAlg, parsed.keyBytes) } return didKey } @@ -106,13 +112,16 @@ const getServiceEndpoint = ( doc: DidDocument, opts: { id: string; type: string }, ) => { + const did = getDid(doc) let services = doc.service if (!services) return undefined if (typeof services !== 'object') return undefined if (!Array.isArray(services)) { services = [services] } - const found = services.find((service) => service.id === opts.id) + const found = services.find( + (service) => service.id === opts.id || service.id === `${did}${opts.id}`, + ) if (!found) return undefined if (found.type !== opts.type) { return undefined diff --git a/packages/identity/tests/did-document.test.ts b/packages/identity/tests/did-document.test.ts new file mode 100644 index 00000000000..c15759385da --- /dev/null +++ b/packages/identity/tests/did-document.test.ts @@ -0,0 +1,103 @@ +import { DidResolver, ensureAtpDocument } from '../src' + +describe('did parsing', () => { + it('throws on bad DID document', async () => { + const did = 'did:plc:yk4dd2qkboz2yv6tpubpc6co' + const docJson = `{ + "ideep": "did:plc:yk4dd2qkboz2yv6tpubpc6co", + "blah": [ + "https://dholms.xyz" + ], + "zoot": [ + { + "id": "#elsewhere", + "type": "EcdsaSecp256k1VerificationKey2019", + "controller": "did:plc:yk4dd2qkboz2yv6tpubpc6co", + "publicKeyMultibase": "zQYEBzXeuTM9UR3rfvNag6L3RNAs5pQZyYPsomTsgQhsxLdEgCrPTLgFna8yqCnxPpNT7DBk6Ym3dgPKNu86vt9GR" + } + ], + "yarg": [ ] +}` + const resolver = new DidResolver({}) + expect(() => { + resolver.validateDidDoc(did, JSON.parse(docJson)) + }).toThrow() + }) + + it('parses legacy DID format, extracts atpData', async () => { + const did = 'did:plc:yk4dd2qkboz2yv6tpubpc6co' + const docJson = `{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/secp256k1-2019/v1" + ], + "id": "did:plc:yk4dd2qkboz2yv6tpubpc6co", + "alsoKnownAs": [ + "at://dholms.xyz" + ], + "verificationMethod": [ + { + "id": "#atproto", + "type": "EcdsaSecp256k1VerificationKey2019", + "controller": "did:plc:yk4dd2qkboz2yv6tpubpc6co", + "publicKeyMultibase": "zQYEBzXeuTM9UR3rfvNag6L3RNAs5pQZyYPsomTsgQhsxLdEgCrPTLgFna8yqCnxPpNT7DBk6Ym3dgPKNu86vt9GR" + } + ], + "service": [ + { + "id": "#atproto_pds", + "type": "AtprotoPersonalDataServer", + "serviceEndpoint": "https://bsky.social" + } + ] +}` + const resolver = new DidResolver({}) + const doc = resolver.validateDidDoc(did, JSON.parse(docJson)) + const atpData = ensureAtpDocument(doc) + expect(atpData.did).toEqual(did) + expect(atpData.handle).toEqual('dholms.xyz') + expect(atpData.pds).toEqual('https://bsky.social') + expect(atpData.signingKey).toEqual( + 'did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF', + ) + }) + + it('parses newer Multikey DID format, extracts atpData', async () => { + const did = 'did:plc:yk4dd2qkboz2yv6tpubpc6co' + const docJson = `{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/multikey/v1", + "https://w3id.org/security/suites/secp256k1-2019/v1" + ], + "id": "did:plc:yk4dd2qkboz2yv6tpubpc6co", + "alsoKnownAs": [ + "at://dholms.xyz" + ], + "verificationMethod": [ + { + "id": "did:plc:yk4dd2qkboz2yv6tpubpc6co#atproto", + "type": "Multikey", + "controller": "did:plc:yk4dd2qkboz2yv6tpubpc6co", + "publicKeyMultibase": "zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" + } + ], + "service": [ + { + "id": "#atproto_pds", + "type": "AtprotoPersonalDataServer", + "serviceEndpoint": "https://bsky.social" + } + ] +}` + const resolver = new DidResolver({}) + const doc = resolver.validateDidDoc(did, JSON.parse(docJson)) + const atpData = ensureAtpDocument(doc) + expect(atpData.did).toEqual(did) + expect(atpData.handle).toEqual('dholms.xyz') + expect(atpData.pds).toEqual('https://bsky.social') + expect(atpData.signingKey).toEqual( + 'did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF', + ) + }) +}) From 46d7672476db94a5b2cc1eebf82958c4f577b3e3 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Fri, 25 Aug 2023 10:49:17 -0500 Subject: [PATCH 194/237] Fix dependencies in dockerfiles (#1519) * fix dependencies in dockerfiles * fix hanging reference to uri --- packages/bsky/Dockerfile | 4 +--- packages/bsky/tests/reprocessing.test.ts | 2 +- packages/pds/Dockerfile | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/bsky/Dockerfile b/packages/bsky/Dockerfile index 43028908da4..168e1316214 100644 --- a/packages/bsky/Dockerfile +++ b/packages/bsky/Dockerfile @@ -11,11 +11,9 @@ COPY ./packages/common ./packages/common COPY ./packages/common-web ./packages/common-web COPY ./packages/crypto ./packages/crypto COPY ./packages/identity ./packages/identity -COPY ./packages/identifier ./packages/identifier +COPY ./packages/syntax ./packages/syntax COPY ./packages/lexicon ./packages/lexicon -COPY ./packages/nsid ./packages/nsid COPY ./packages/repo ./packages/repo -COPY ./packages/uri ./packages/uri COPY ./packages/xrpc ./packages/xrpc COPY ./packages/xrpc-server ./packages/xrpc-server RUN ATP_BUILD_SHALLOW=true yarn install --frozen-lockfile > /dev/null diff --git a/packages/bsky/tests/reprocessing.test.ts b/packages/bsky/tests/reprocessing.test.ts index b45f252df07..dd170c570ab 100644 --- a/packages/bsky/tests/reprocessing.test.ts +++ b/packages/bsky/tests/reprocessing.test.ts @@ -1,5 +1,5 @@ import axios from 'axios' -import { AtUri } from '@atproto/uri' +import { AtUri } from '@atproto/syntax' import AtpAgent from '@atproto/api' import { TestNetwork } from '@atproto/dev-env' import { SeedClient } from './seeds/client' diff --git a/packages/pds/Dockerfile b/packages/pds/Dockerfile index c3340350669..01d4b74654c 100644 --- a/packages/pds/Dockerfile +++ b/packages/pds/Dockerfile @@ -10,14 +10,12 @@ COPY ./packages/aws ./packages/aws COPY ./packages/common ./packages/common COPY ./packages/common-web ./packages/common-web COPY ./packages/crypto ./packages/crypto -COPY ./packages/identifier ./packages/identifier +COPY ./packages/syntax ./packages/syntax COPY ./packages/identity ./packages/identity COPY ./packages/lex-cli ./packages/lex-cli COPY ./packages/lexicon ./packages/lexicon -COPY ./packages/nsid ./packages/nsid COPY ./packages/pds ./packages/pds COPY ./packages/repo ./packages/repo -COPY ./packages/uri ./packages/uri COPY ./packages/xrpc ./packages/xrpc COPY ./packages/xrpc-server ./packages/xrpc-server RUN ATP_BUILD_SHALLOW=true yarn install --frozen-lockfile > /dev/null From acafe8e986b3dee9b01298a83e99fd6a6ef94471 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Fri, 25 Aug 2023 17:19:35 -0400 Subject: [PATCH 195/237] Stop indexing post hierarchy on pds (#1358) * serve pds post threads w/o post_hierarchy, continue to index post_hierarchy * add missing files * remove post-hierarchy table from pds * uncomment migration --------- Co-authored-by: dholms --- packages/pds/src/app-view/db/index.ts | 2 -- .../src/app-view/db/tables/post-hierarchy.ts | 10 ------- .../services/indexing/plugins/post.ts | 30 ------------------- ...230824T182048120Z-remove-post-hierarchy.ts | 4 --- 4 files changed, 46 deletions(-) delete mode 100644 packages/pds/src/app-view/db/tables/post-hierarchy.ts diff --git a/packages/pds/src/app-view/db/index.ts b/packages/pds/src/app-view/db/index.ts index e0eefa54edd..21edd709a7a 100644 --- a/packages/pds/src/app-view/db/index.ts +++ b/packages/pds/src/app-view/db/index.ts @@ -4,7 +4,6 @@ import * as profileAgg from './tables/profile-agg' import * as post from './tables/post' import * as postAgg from './tables/post-agg' import * as postEmbed from './tables/post-embed' -import * as postHierarchy from './tables/post-hierarchy' import * as repost from './tables/repost' import * as feedItem from './tables/feed-item' import * as follow from './tables/follow' @@ -25,7 +24,6 @@ export type DatabaseSchemaType = duplicateRecords.PartialDB & post.PartialDB & postAgg.PartialDB & postEmbed.PartialDB & - postHierarchy.PartialDB & repost.PartialDB & feedItem.PartialDB & follow.PartialDB & diff --git a/packages/pds/src/app-view/db/tables/post-hierarchy.ts b/packages/pds/src/app-view/db/tables/post-hierarchy.ts deleted file mode 100644 index 70c44f67ce4..00000000000 --- a/packages/pds/src/app-view/db/tables/post-hierarchy.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const tableName = 'post_hierarchy' -export interface PostHierarchy { - uri: string - ancestorUri: string - depth: number -} - -export type PartialDB = { - [tableName]: PostHierarchy -} diff --git a/packages/pds/src/app-view/services/indexing/plugins/post.ts b/packages/pds/src/app-view/services/indexing/plugins/post.ts index 37d07539879..3583f087cf1 100644 --- a/packages/pds/src/app-view/services/indexing/plugins/post.ts +++ b/packages/pds/src/app-view/services/indexing/plugins/post.ts @@ -1,4 +1,3 @@ -import { sql } from 'kysely' import { CID } from 'multiformats/cid' import { AtUri } from '@atproto/syntax' import { Record as PostRecord } from '../../../../lexicon/types/app/bsky/feed/post' @@ -139,35 +138,6 @@ const insertFn = async ( await db.insertInto('post_embed_record').values(recordEmbed).execute() } } - // Thread index - // @TODO remove thread hierarchy indexing - await db - .insertInto('post_hierarchy') - .values({ - uri: post.uri, - ancestorUri: post.uri, - depth: 0, - }) - .onConflict((oc) => oc.doNothing()) // Supports post updates - .execute() - if (post.replyParent) { - await db - .insertInto('post_hierarchy') - .columns(['uri', 'ancestorUri', 'depth']) - .expression( - db - .selectFrom('post_hierarchy as parent_hierarchy') - .where('parent_hierarchy.uri', '=', post.replyParent) - .select([ - sql`${post.uri}`.as('uri'), - 'ancestorUri', - sql`depth + 1`.as('depth'), - ]), - ) - .onConflict((oc) => oc.doNothing()) // Supports post updates - .returningAll() - .execute() - } const ancestors = await getAncestorsAndSelfQb(db, { uri: post.uri, parentHeight: REPLY_NOTIF_DEPTH, diff --git a/packages/pds/src/db/migrations/20230824T182048120Z-remove-post-hierarchy.ts b/packages/pds/src/db/migrations/20230824T182048120Z-remove-post-hierarchy.ts index 0c26b7da10d..a4bdf1b370d 100644 --- a/packages/pds/src/db/migrations/20230824T182048120Z-remove-post-hierarchy.ts +++ b/packages/pds/src/db/migrations/20230824T182048120Z-remove-post-hierarchy.ts @@ -1,7 +1,6 @@ import { Kysely, sql } from 'kysely' export async function up(db: Kysely): Promise { - /* @TODO these migrations will be run manually in prod await db.schema.dropTable('post_hierarchy').execute() // recreate index that calculates e.g. "replyCount", turning it into a covering index // for uri so that recursive query for post descendents can use an index-only scan. @@ -9,11 +8,9 @@ export async function up(db: Kysely): Promise { db, ) await db.schema.dropIndex('post_replyparent_idx').execute() - */ } export async function down(db: Kysely): Promise { - /* @TODO these migrations will be run manually in prod await db.schema .createTable('post_hierarchy') .addColumn('uri', 'varchar', (col) => col.notNull()) @@ -32,5 +29,4 @@ export async function down(db: Kysely): Promise { .on('post') .column('replyParent') .execute() - */ } From 895a21fc25be29c6bd5e84281878e18808b68859 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Fri, 25 Aug 2023 17:43:40 -0500 Subject: [PATCH 196/237] Clean up block & mute handling for push notifs (#1520) clean up block & mute handling for push notifs --- packages/bsky/src/notifications.ts | 57 +++++++++++-- .../bsky/tests/notification-server.test.ts | 85 ++++++++++++++++++- 2 files changed, 135 insertions(+), 7 deletions(-) diff --git a/packages/bsky/src/notifications.ts b/packages/bsky/src/notifications.ts index 89a1ab7925c..fdf24919d19 100644 --- a/packages/bsky/src/notifications.ts +++ b/packages/bsky/src/notifications.ts @@ -1,5 +1,5 @@ import axios from 'axios' -import { Insertable } from 'kysely' +import { Insertable, sql } from 'kysely' import TTLCache from '@isaacs/ttlcache' import { AtUri } from '@atproto/api' import { MINUTE, chunkArray } from '@atproto/common' @@ -7,7 +7,7 @@ import Database from './db/primary' import { Notification } from './db/tables/notification' import { NotificationPushToken as PushToken } from './db/tables/notification-push-token' import logger from './indexer/logger' -import { notSoftDeletedClause } from './db/util' +import { notSoftDeletedClause, valuesList } from './db/util' import { ids } from './lexicon/lexicons' import { retryHttp } from './util/retry' @@ -188,7 +188,7 @@ export class NotificationServer { const allUris = [...subjectUris, ...recordUris] // gather potential display data for notifications in batch - const [authors, posts] = await Promise.all([ + const [authors, posts, blocksAndMutes] = await Promise.all([ this.db.db .selectFrom('actor') .leftJoin('profile', 'profile.creator', 'actor.did') @@ -207,6 +207,7 @@ export class NotificationServer { .where('post.uri', 'in', allUris.length ? allUris : ['']) .select(['post.uri as uri', 'text']) .execute(), + this.findBlocksAndMutes(notifs), ]) const authorsByDid = authors.reduce((acc, author) => { @@ -233,8 +234,12 @@ export class NotificationServer { const postRecord = postsByUri[recordUri] const postSubject = subjectUri ? postsByUri[subjectUri] : null - // if no display name, dont send notification - if (!author) { + // if blocked or muted, don't send notification + const shouldFilter = blocksAndMutes.some( + (pair) => pair.author === notif.author && pair.receiver === notif.did, + ) + if (shouldFilter || !author) { + // if no display name, dont send notification continue } // const author = displayName.displayName @@ -304,6 +309,48 @@ export class NotificationServer { return results } + + async findBlocksAndMutes(notifs: InsertableNotif[]) { + const pairs = notifs.map((n) => ({ author: n.author, receiver: n.did })) + const { ref } = this.db.db.dynamic + const blockQb = this.db.db + .selectFrom('actor_block') + .where((outer) => + outer + .where((qb) => + qb + .whereRef('actor_block.creator', '=', ref('author')) + .whereRef('actor_block.subjectDid', '=', ref('receiver')), + ) + .orWhere((qb) => + qb + .whereRef('actor_block.creator', '=', ref('receiver')) + .whereRef('actor_block.subjectDid', '=', ref('author')), + ), + ) + .select(['creator', 'subjectDid']) + const muteQb = this.db.db + .selectFrom('mute') + .whereRef('mute.subjectDid', '=', ref('author')) + .whereRef('mute.mutedByDid', '=', ref('receiver')) + .selectAll() + const muteListQb = this.db.db + .selectFrom('list_item') + .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') + .whereRef('list_mute.mutedByDid', '=', ref('receiver')) + .whereRef('list_item.subjectDid', '=', ref('author')) + .select('list_item.subjectDid') + + const values = valuesList(pairs.map((p) => sql`${p.author}, ${p.receiver}`)) + const filterPairs = await this.db.db + .selectFrom(values.as(sql`pair (author, receiver)`)) + .whereExists(muteQb) + .orWhereExists(muteListQb) + .orWhereExists(blockQb) + .selectAll() + .execute() + return filterPairs as { author: string; receiver: string }[] + } } const isRecent = (isoTime: string, timeDiff: number): boolean => { diff --git a/packages/bsky/tests/notification-server.test.ts b/packages/bsky/tests/notification-server.test.ts index c18f4b96e3e..aeb7f8ae97c 100644 --- a/packages/bsky/tests/notification-server.test.ts +++ b/packages/bsky/tests/notification-server.test.ts @@ -1,4 +1,4 @@ -import AtpAgent from '@atproto/api' +import AtpAgent, { AtUri } from '@atproto/api' import { TestNetwork } from '@atproto/dev-env' import { SeedClient } from './seeds/client' import basicSeed from './seeds/basic' @@ -8,6 +8,7 @@ import { Database } from '../src' describe('notification server', () => { let network: TestNetwork let agent: AtpAgent + let pdsAgent: AtpAgent let sc: SeedClient let notifServer: NotificationServer @@ -19,7 +20,7 @@ describe('notification server', () => { dbPostgresSchema: 'bsky_notification_server', }) agent = network.bsky.getClient() - const pdsAgent = network.pds.getClient() + pdsAgent = network.pds.getClient() sc = new SeedClient(pdsAgent) await basicSeed(sc) await network.processAll() @@ -111,6 +112,86 @@ describe('notification server', () => { expect(attrs[0].title).toEqual('bobby liked your post') }) + it('filters notifications that violate blocks', async () => { + const db = network.bsky.ctx.db.getPrimary() + const notif = await getLikeNotification(db, alice) + if (!notif) throw new Error('no notification found') + const blockRef = await pdsAgent.api.app.bsky.graph.block.create( + { repo: alice }, + { subject: notif.author, createdAt: new Date().toISOString() }, + sc.getHeaders(alice), + ) + await network.processAll() + // verify inverse of block + const flippedNotif = { + ...notif, + did: notif.author, + author: notif.did, + } + const attrs = await notifServer.getNotificationDisplayAttributes([ + notif, + flippedNotif, + ]) + expect(attrs.length).toBe(0) + const uri = new AtUri(blockRef.uri) + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: alice, rkey: uri.rkey }, + sc.getHeaders(alice), + ) + await network.processAll() + }) + + it('filters notifications that violate mutes', async () => { + const db = network.bsky.ctx.db.getPrimary() + const notif = await getLikeNotification(db, alice) + if (!notif) throw new Error('no notification found') + await pdsAgent.api.app.bsky.graph.muteActor( + { actor: notif.author }, + { headers: sc.getHeaders(alice), encoding: 'application/json' }, + ) + const attrs = await notifServer.getNotificationDisplayAttributes([notif]) + expect(attrs.length).toBe(0) + await pdsAgent.api.app.bsky.graph.unmuteActor( + { actor: notif.author }, + { headers: sc.getHeaders(alice), encoding: 'application/json' }, + ) + }) + + it('filters notifications that violate mutelists', async () => { + const db = network.bsky.ctx.db.getPrimary() + const notif = await getLikeNotification(db, alice) + if (!notif) throw new Error('no notification found') + const listRef = await pdsAgent.api.app.bsky.graph.list.create( + { repo: alice }, + { + name: 'mute', + purpose: 'app.bsky.graph.defs#modlist', + createdAt: new Date().toISOString(), + }, + sc.getHeaders(alice), + ) + await pdsAgent.api.app.bsky.graph.listitem.create( + { repo: alice }, + { + subject: notif.author, + list: listRef.uri, + createdAt: new Date().toISOString(), + }, + sc.getHeaders(alice), + ) + await network.processAll() + await pdsAgent.api.app.bsky.graph.muteActorList( + { list: listRef.uri }, + { headers: sc.getHeaders(alice), encoding: 'application/json' }, + ) + const attrs = await notifServer.getNotificationDisplayAttributes([notif]) + expect(attrs.length).toBe(0) + await pdsAgent.api.app.bsky.graph.unmuteActorList( + { list: listRef.uri }, + { headers: sc.getHeaders(alice), encoding: 'application/json' }, + ) + }) + it('prepares notification to be sent', async () => { const db = network.bsky.ctx.db.getPrimary() const notif = await getLikeNotification(db, alice) From 964e08ad824723a18f3ff1b469fa20b9666e8691 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Mon, 28 Aug 2023 15:39:57 +0200 Subject: [PATCH 197/237] :bug: Add labels caching as a config for pds and enable it when running dev-env (#1511) * :bug: Add labels caching as a config for pds and enable it when running dev-env * :sparkles: Move caching enable config to bin.ts --- packages/dev-env/src/bin-network.ts | 1 + packages/dev-env/src/bin.ts | 6 +++++- packages/dev-env/src/pds.ts | 2 +- packages/dev-env/src/types.ts | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/dev-env/src/bin-network.ts b/packages/dev-env/src/bin-network.ts index 56f0138b3b2..193c7ea968a 100644 --- a/packages/dev-env/src/bin-network.ts +++ b/packages/dev-env/src/bin-network.ts @@ -16,6 +16,7 @@ const run = async () => { pds: { port: 2583, publicUrl: 'http://localhost:2583', + enableLabelsCache: true, dbPostgresSchema: 'pds', }, bsky: { diff --git a/packages/dev-env/src/bin.ts b/packages/dev-env/src/bin.ts index 3028f6fbcc9..532265e18ec 100644 --- a/packages/dev-env/src/bin.ts +++ b/packages/dev-env/src/bin.ts @@ -13,7 +13,11 @@ const run = async () => { [ created by Bluesky ]`) const network = await TestNetworkNoAppView.create({ - pds: { port: 2583, publicUrl: 'http://localhost:2583' }, + pds: { + port: 2583, + enableLabelsCache: true, + publicUrl: 'http://localhost:2583', + }, plc: { port: 2582 }, }) await generateMockSetup(network) diff --git a/packages/dev-env/src/pds.ts b/packages/dev-env/src/pds.ts index 2ee457837e0..97b8ceb7452 100644 --- a/packages/dev-env/src/pds.ts +++ b/packages/dev-env/src/pds.ts @@ -103,7 +103,7 @@ export class TestPds { await server.start() // we refresh label cache by hand in `processAll` instead of on a timer - server.ctx.labelCache.stop() + if (!cfg.enableLabelsCache) server.ctx.labelCache.stop() return new TestPds(url, port, server) } diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index 4ea9cb002d8..cd7d029debc 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -11,6 +11,7 @@ export type PdsConfig = Partial & { migration?: string enableInProcessAppView?: boolean algos?: pds.MountedAlgos + enableLabelsCache?: boolean } export type BskyConfig = Partial & { From cc231b5584cb07e4b51af7d3eff4fca5c73cdcb8 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 28 Aug 2023 14:56:37 -0500 Subject: [PATCH 198/237] Fix broken sqlite migration (#1526) fix broken sqlite migration --- ...0230824T182048120Z-remove-post-hierarchy.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/pds/src/db/migrations/20230824T182048120Z-remove-post-hierarchy.ts b/packages/pds/src/db/migrations/20230824T182048120Z-remove-post-hierarchy.ts index a4bdf1b370d..8144534af07 100644 --- a/packages/pds/src/db/migrations/20230824T182048120Z-remove-post-hierarchy.ts +++ b/packages/pds/src/db/migrations/20230824T182048120Z-remove-post-hierarchy.ts @@ -1,12 +1,22 @@ import { Kysely, sql } from 'kysely' +import { Dialect } from '..' -export async function up(db: Kysely): Promise { +export async function up(db: Kysely, dialect: Dialect): Promise { await db.schema.dropTable('post_hierarchy').execute() // recreate index that calculates e.g. "replyCount", turning it into a covering index // for uri so that recursive query for post descendents can use an index-only scan. - await sql`create index "post_replyparent_uri_idx" on "post" ("replyParent") include ("uri")`.execute( - db, - ) + if (dialect === 'pg') { + await sql`create index "post_replyparent_uri_idx" on "post" ("replyParent") include ("uri")`.execute( + db, + ) + } else { + // in sqlite, just index on uri as well + await db.schema + .createIndex('post_replyparent_uri_idx') + .on('post') + .columns(['replyParent', 'uri']) + .execute() + } await db.schema.dropIndex('post_replyparent_idx').execute() } From 89917d7fc69c175b81f7ae44d18d8882aef827a8 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 28 Aug 2023 15:00:24 -0500 Subject: [PATCH 199/237] Bump pkg versions (#1525) * v0.2.0 * bump xrpc --- packages/identity/package.json | 2 +- packages/xrpc/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/identity/package.json b/packages/identity/package.json index f8e507c0672..bfe0e06b82b 100644 --- a/packages/identity/package.json +++ b/packages/identity/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/identity", - "version": "0.1.0", + "version": "0.2.0", "main": "src/index.ts", "license": "MIT", "repository": { diff --git a/packages/xrpc/package.json b/packages/xrpc/package.json index 63ad8955ce9..5f45f71f95f 100644 --- a/packages/xrpc/package.json +++ b/packages/xrpc/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/xrpc", - "version": "0.2.0", + "version": "0.3.0", "main": "src/index.ts", "scripts": { "prettier": "prettier --check src/", From d0a3d7070010d2b8145369c3333e0c9ca6265160 Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 28 Aug 2023 19:04:30 -0500 Subject: [PATCH 200/237] v0.2.1 --- packages/crypto/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/crypto/package.json b/packages/crypto/package.json index bccaaa42c12..048a97afd0e 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/crypto", - "version": "0.2.0", + "version": "0.2.1", "main": "src/index.ts", "license": "MIT", "repository": { From 913c912bc400b8f4f2976d1ddfd91ca271870408 Mon Sep 17 00:00:00 2001 From: dholms Date: Mon, 28 Aug 2023 19:10:53 -0500 Subject: [PATCH 201/237] v0.2.2 --- packages/crypto/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 048a97afd0e..511d5aa105d 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/crypto", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "license": "MIT", "repository": { From 70c8159644415eaacc3e60e6c166d572185e7700 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 29 Aug 2023 14:30:26 -0500 Subject: [PATCH 202/237] Fix ambiguous reference to `repoRev` (#1534) fix ambiguous column --- packages/pds/src/services/local/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pds/src/services/local/index.ts b/packages/pds/src/services/local/index.ts index a850a46c55e..867f3baf4e7 100644 --- a/packages/pds/src/services/local/index.ts +++ b/packages/pds/src/services/local/index.ts @@ -93,8 +93,8 @@ export class LocalService { 'record.indexedAt', ]) .where('did', '=', did) - .where('repoRev', '>', rev) - .orderBy('repoRev', 'asc') + .where('record.repoRev', '>', rev) + .orderBy('record.repoRev', 'asc') .execute() return res.reduce( (acc, cur) => { From ad1fcf138711139ceedcf63db3702bf3b6cdb753 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 29 Aug 2023 19:07:21 -0500 Subject: [PATCH 203/237] Repo history rewrite (#1479) * logical changes in repo * tests * tweak commit data * building pds * patching up some tests * tidy + more tests * patch up bsky * clean up db * db migration * small patches * fix up another test * reinclude prevs * api & lex updates * add back in deprecated routes * backward compatibility for commit v2 * add upgrade repo version root * move namespace * migration test * patch up a few more tests * remove deprecated rebase routes * tweak api * sprinkle rev around a few more places * getCurrent -> getLatestCommit * fix test * pr feedback & tidy * fix up block pagination * tidy again * add in a tets * add cursor to listBlobs * getRepo rev -> since * clean up proofs test * dont change getHead * tweak migrate route * build branch * hit index in block range query * check for dupe record refs * tidy * set prev to null * dont build branch --- lexicons/com/atproto/admin/rebaseRepo.json | 33 -- lexicons/com/atproto/repo/rebaseRepo.json | 33 -- lexicons/com/atproto/sync/getCheckout.json | 7 +- lexicons/com/atproto/sync/getCommitPath.json | 44 --- lexicons/com/atproto/sync/getHead.json | 2 +- .../com/atproto/sync/getLatestCommit.json | 35 +++ lexicons/com/atproto/sync/getRepo.json | 11 +- lexicons/com/atproto/sync/listBlobs.json | 8 +- lexicons/com/atproto/sync/subscribeRepos.json | 12 +- .../com/atproto/temp/upgradeRepoVersion.json | 20 ++ packages/api/src/client/index.ts | 75 +++-- packages/api/src/client/lexicons.ts | 205 +++++-------- .../types/com/atproto/admin/rebaseRepo.ts | 49 --- .../types/com/atproto/sync/getCheckout.ts | 2 - .../{getCommitPath.ts => getLatestCommit.ts} | 14 +- .../client/types/com/atproto/sync/getRepo.ts | 6 +- .../types/com/atproto/sync/listBlobs.ts | 9 +- .../types/com/atproto/sync/subscribeRepos.ts | 6 +- .../upgradeRepoVersion.ts} | 19 +- packages/bsky/src/lexicon/index.ts | 64 ++-- packages/bsky/src/lexicon/lexicons.ts | 205 +++++-------- .../types/com/atproto/repo/rebaseRepo.ts | 42 --- .../types/com/atproto/sync/getCheckout.ts | 2 - .../com/atproto/sync/getLatestCommit.ts} | 8 +- .../lexicon/types/com/atproto/sync/getRepo.ts | 6 +- .../types/com/atproto/sync/listBlobs.ts | 9 +- .../types/com/atproto/sync/subscribeRepos.ts | 6 +- .../com/atproto/temp/upgradeRepoVersion.ts} | 6 +- packages/bsky/src/services/indexing/index.ts | 42 +-- packages/bsky/tests/indexing.test.ts | 27 +- .../pds/src/api/com/atproto/admin/index.ts | 2 - .../src/api/com/atproto/admin/rebaseRepo.ts | 29 -- packages/pds/src/api/com/atproto/index.ts | 2 + .../pds/src/api/com/atproto/repo/index.ts | 2 - .../src/api/com/atproto/repo/rebaseRepo.ts | 41 --- .../atproto/sync/deprecated/getCheckout.ts | 42 +++ .../atproto/sync/{ => deprecated}/getHead.ts | 10 +- .../pds/src/api/com/atproto/sync/getBlocks.ts | 5 +- .../src/api/com/atproto/sync/getCommitPath.ts | 43 --- .../{getCheckout.ts => getLatestCommit.ts} | 26 +- .../pds/src/api/com/atproto/sync/getRecord.ts | 2 +- .../pds/src/api/com/atproto/sync/getRepo.ts | 39 +-- .../pds/src/api/com/atproto/sync/index.ts | 12 +- .../pds/src/api/com/atproto/sync/listBlobs.ts | 36 ++- .../src/api/com/atproto/upgradeRepoVersion.ts | 117 +++++++ .../api/app/bsky/util/read-after-write.ts | 2 + packages/pds/src/db/database-schema.ts | 6 - .../20230201T200606704Z-repo-sync-data-pt2.ts | 143 +-------- ...0230828T153013575Z-repo-history-rewrite.ts | 62 ++++ packages/pds/src/db/migrations/index.ts | 1 + packages/pds/src/db/tables/ipld-block.ts | 1 + packages/pds/src/db/tables/repo-blob.ts | 2 +- .../pds/src/db/tables/repo-commit-block.ts | 9 - .../pds/src/db/tables/repo-commit-history.ts | 9 - packages/pds/src/db/tables/repo-root.ts | 1 + packages/pds/src/lexicon/index.ts | 64 ++-- packages/pds/src/lexicon/lexicons.ts | 205 +++++-------- .../types/com/atproto/repo/rebaseRepo.ts | 42 --- .../types/com/atproto/sync/getCheckout.ts | 2 - .../com/atproto/sync/getLatestCommit.ts} | 8 +- .../lexicon/types/com/atproto/sync/getRepo.ts | 6 +- .../types/com/atproto/sync/listBlobs.ts | 9 +- .../types/com/atproto/sync/subscribeRepos.ts | 6 +- .../com/atproto/temp/upgradeRepoVersion.ts} | 6 +- packages/pds/src/logger.ts | 1 + packages/pds/src/sequencer/events.ts | 39 +-- packages/pds/src/services/moderation/index.ts | 2 +- packages/pds/src/services/repo/blobs.ts | 28 +- packages/pds/src/services/repo/index.ts | 173 +++-------- packages/pds/src/sql-repo-storage.ts | 289 +++++++----------- packages/pds/tests/account-deletion.test.ts | 29 -- packages/pds/tests/crud.test.ts | 69 ++++- packages/pds/tests/db.test.ts | 2 + .../migrations/repo-version-upgrade.test.ts | 173 +++++++++++ packages/pds/tests/races.test.ts | 20 +- packages/pds/tests/rebase.test.ts | 81 ----- packages/pds/tests/sql-repo-storage.test.ts | 50 +-- .../pds/tests/sync/subscribe-repos.test.ts | 125 +++----- packages/pds/tests/sync/sync.test.ts | 232 ++------------ packages/repo/src/data-diff.ts | 90 +++--- packages/repo/src/index.ts | 1 - packages/repo/src/mst/diff.ts | 50 +-- packages/repo/src/mst/mst.ts | 18 +- packages/repo/src/readable-repo.ts | 6 +- packages/repo/src/repo.ts | 117 +++---- packages/repo/src/storage/index.ts | 1 - .../repo/src/storage/memory-blockstore.ts | 95 ++---- packages/repo/src/storage/repo-storage.ts | 43 --- packages/repo/src/storage/types.ts | 29 ++ packages/repo/src/sync/consumer.ts | 286 ++++++++++------- packages/repo/src/sync/provider.ts | 42 +-- packages/repo/src/types.ts | 75 +++-- packages/repo/src/util.ts | 103 ++----- packages/repo/src/verify.ts | 268 ---------------- packages/repo/tests/_util.ts | 105 +++---- packages/repo/tests/mst.test.ts | 5 +- .../{sync/narrow.test.ts => proofs.test.ts} | 29 +- packages/repo/tests/rebase.test.ts | 37 --- packages/repo/tests/repo.test.ts | 9 +- packages/repo/tests/sync.test.ts | 97 ++++++ packages/repo/tests/sync/checkout.test.ts | 75 ----- packages/repo/tests/sync/diff.test.ts | 92 ------ 102 files changed, 1853 insertions(+), 3132 deletions(-) delete mode 100644 lexicons/com/atproto/admin/rebaseRepo.json delete mode 100644 lexicons/com/atproto/repo/rebaseRepo.json delete mode 100644 lexicons/com/atproto/sync/getCommitPath.json create mode 100644 lexicons/com/atproto/sync/getLatestCommit.json create mode 100644 lexicons/com/atproto/temp/upgradeRepoVersion.json delete mode 100644 packages/api/src/client/types/com/atproto/admin/rebaseRepo.ts rename packages/api/src/client/types/com/atproto/sync/{getCommitPath.ts => getLatestCommit.ts} (73%) rename packages/api/src/client/types/com/atproto/{repo/rebaseRepo.ts => temp/upgradeRepoVersion.ts} (52%) delete mode 100644 packages/bsky/src/lexicon/types/com/atproto/repo/rebaseRepo.ts rename packages/{pds/src/lexicon/types/com/atproto/sync/getCommitPath.ts => bsky/src/lexicon/types/com/atproto/sync/getLatestCommit.ts} (89%) rename packages/{pds/src/lexicon/types/com/atproto/admin/rebaseRepo.ts => bsky/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts} (83%) delete mode 100644 packages/pds/src/api/com/atproto/admin/rebaseRepo.ts delete mode 100644 packages/pds/src/api/com/atproto/repo/rebaseRepo.ts create mode 100644 packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts rename packages/pds/src/api/com/atproto/sync/{ => deprecated}/getHead.ts (79%) delete mode 100644 packages/pds/src/api/com/atproto/sync/getCommitPath.ts rename packages/pds/src/api/com/atproto/sync/{getCheckout.ts => getLatestCommit.ts} (56%) create mode 100644 packages/pds/src/api/com/atproto/upgradeRepoVersion.ts create mode 100644 packages/pds/src/db/migrations/20230828T153013575Z-repo-history-rewrite.ts delete mode 100644 packages/pds/src/db/tables/repo-commit-block.ts delete mode 100644 packages/pds/src/db/tables/repo-commit-history.ts delete mode 100644 packages/pds/src/lexicon/types/com/atproto/repo/rebaseRepo.ts rename packages/{bsky/src/lexicon/types/com/atproto/sync/getCommitPath.ts => pds/src/lexicon/types/com/atproto/sync/getLatestCommit.ts} (89%) rename packages/{bsky/src/lexicon/types/com/atproto/admin/rebaseRepo.ts => pds/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts} (83%) create mode 100644 packages/pds/tests/migrations/repo-version-upgrade.test.ts delete mode 100644 packages/pds/tests/rebase.test.ts delete mode 100644 packages/repo/src/storage/repo-storage.ts delete mode 100644 packages/repo/src/verify.ts rename packages/repo/tests/{sync/narrow.test.ts => proofs.test.ts} (80%) delete mode 100644 packages/repo/tests/rebase.test.ts create mode 100644 packages/repo/tests/sync.test.ts delete mode 100644 packages/repo/tests/sync/checkout.test.ts delete mode 100644 packages/repo/tests/sync/diff.test.ts diff --git a/lexicons/com/atproto/admin/rebaseRepo.json b/lexicons/com/atproto/admin/rebaseRepo.json deleted file mode 100644 index 86e75c59a52..00000000000 --- a/lexicons/com/atproto/admin/rebaseRepo.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "lexicon": 1, - "id": "com.atproto.admin.rebaseRepo", - "defs": { - "main": { - "type": "procedure", - "description": "Administrative action to rebase an account's repo", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["repo"], - "properties": { - "repo": { - "type": "string", - "format": "at-identifier", - "description": "The handle or DID of the repo." - }, - "swapCommit": { - "type": "string", - "format": "cid", - "description": "Compare and swap with the previous commit by cid." - } - } - } - }, - "errors": [ - {"name": "InvalidSwap"}, - {"name": "ConcurrentWrites"} - ] - } - } -} diff --git a/lexicons/com/atproto/repo/rebaseRepo.json b/lexicons/com/atproto/repo/rebaseRepo.json deleted file mode 100644 index ff278c0db3b..00000000000 --- a/lexicons/com/atproto/repo/rebaseRepo.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "lexicon": 1, - "id": "com.atproto.repo.rebaseRepo", - "defs": { - "main": { - "type": "procedure", - "description": "Simple rebase of repo that deletes history", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["repo"], - "properties": { - "repo": { - "type": "string", - "format": "at-identifier", - "description": "The handle or DID of the repo." - }, - "swapCommit": { - "type": "string", - "format": "cid", - "description": "Compare and swap with the previous commit by cid." - } - } - } - }, - "errors": [ - {"name": "InvalidSwap"}, - {"name": "ConcurrentWrites"} - ] - } - } -} diff --git a/lexicons/com/atproto/sync/getCheckout.json b/lexicons/com/atproto/sync/getCheckout.json index 16b2330c6fc..0f57cb377cd 100644 --- a/lexicons/com/atproto/sync/getCheckout.json +++ b/lexicons/com/atproto/sync/getCheckout.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Gets the repo state.", + "description": "DEPRECATED - please use com.atproto.sync.getRepo instead", "parameters": { "type": "params", "required": ["did"], @@ -13,11 +13,6 @@ "type": "string", "format": "did", "description": "The DID of the repo." - }, - "commit": { - "type": "string", - "format": "cid", - "description": "The commit to get the checkout from. Defaults to current HEAD." } } }, diff --git a/lexicons/com/atproto/sync/getCommitPath.json b/lexicons/com/atproto/sync/getCommitPath.json deleted file mode 100644 index d011595393c..00000000000 --- a/lexicons/com/atproto/sync/getCommitPath.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "lexicon": 1, - "id": "com.atproto.sync.getCommitPath", - "defs": { - "main": { - "type": "query", - "description": "Gets the path of repo commits", - "parameters": { - "type": "params", - "required": ["did"], - "properties": { - "did": { - "type": "string", - "format": "did", - "description": "The DID of the repo." - }, - "latest": { - "type": "string", - "format": "cid", - "description": "The most recent commit" - }, - "earliest": { - "type": "string", - "format": "cid", - "description": "The earliest commit to start from" - } - } - }, - "output": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["commits"], - "properties": { - "commits": { - "type": "array", - "items": { "type": "string", "format": "cid" } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/lexicons/com/atproto/sync/getHead.json b/lexicons/com/atproto/sync/getHead.json index 38bfe22bce9..bfd110e019b 100644 --- a/lexicons/com/atproto/sync/getHead.json +++ b/lexicons/com/atproto/sync/getHead.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Gets the current HEAD CID of a repo.", + "description": "DEPRECATED - please use com.atproto.sync.getLatestCommit instead", "parameters": { "type": "params", "required": ["did"], diff --git a/lexicons/com/atproto/sync/getLatestCommit.json b/lexicons/com/atproto/sync/getLatestCommit.json new file mode 100644 index 00000000000..8beebdaa434 --- /dev/null +++ b/lexicons/com/atproto/sync/getLatestCommit.json @@ -0,0 +1,35 @@ +{ + "lexicon": 1, + "id": "com.atproto.sync.getLatestCommit", + "defs": { + "main": { + "type": "query", + "description": "Gets 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"} + ] + } + } +} diff --git a/lexicons/com/atproto/sync/getRepo.json b/lexicons/com/atproto/sync/getRepo.json index bf0991db2b4..87ecaa1f796 100644 --- a/lexicons/com/atproto/sync/getRepo.json +++ b/lexicons/com/atproto/sync/getRepo.json @@ -4,7 +4,7 @@ "defs": { "main": { "type": "query", - "description": "Gets the repo state.", + "description": "Gets the did's repo, optionally catching up from a specific revision.", "parameters": { "type": "params", "required": ["did"], @@ -14,15 +14,10 @@ "format": "did", "description": "The DID of the repo." }, - "earliest": { + "since": { "type": "string", "format": "cid", - "description": "The earliest commit in the commit range (not inclusive)" - }, - "latest": { - "type": "string", - "format": "cid", - "description": "The latest commit in the commit range (inclusive)" + "description": "The revision of the repo to catch up from." } } }, diff --git a/lexicons/com/atproto/sync/listBlobs.json b/lexicons/com/atproto/sync/listBlobs.json index 8939a5ee074..7ca3039f6d0 100644 --- a/lexicons/com/atproto/sync/listBlobs.json +++ b/lexicons/com/atproto/sync/listBlobs.json @@ -4,14 +4,15 @@ "defs": { "main": { "type": "query", - "description": "List blob cids for some range of commits", + "description": "List blob cids since some revision", "parameters": { "type": "params", "required": ["did"], "properties": { "did": {"type": "string", "format": "did", "description": "The DID of the repo."}, - "latest": { "type": "string", "format": "cid", "description": "The most recent commit"}, - "earliest": { "type": "string", "format": "cid", "description": "The earliest commit to start from"} + "since": { "type": "string", "format": "cid", "description": "Optional revision of the repo to list blobs since"}, + "limit": {"type": "integer", "minimum": 1, "maximum": 1000, "default": 500}, + "cursor": {"type": "string"} } }, "output": { @@ -20,6 +21,7 @@ "type": "object", "required": ["cids"], "properties": { + "cursor": {"type": "string"}, "cids": { "type": "array", "items": { "type": "string", "format": "cid" } diff --git a/lexicons/com/atproto/sync/subscribeRepos.json b/lexicons/com/atproto/sync/subscribeRepos.json index df26c131ccd..ee3e88a4833 100644 --- a/lexicons/com/atproto/sync/subscribeRepos.json +++ b/lexicons/com/atproto/sync/subscribeRepos.json @@ -33,8 +33,8 @@ }, "commit": { "type": "object", - "required": ["seq", "rebase", "tooBig", "repo", "commit", "prev", "blocks", "ops", "blobs", "time"], - "nullable": ["prev"], + "required": ["seq", "rebase", "tooBig", "repo", "commit", "rev", "since", "blocks", "ops", "blobs", "time"], + "nullable": ["prev", "since"], "properties": { "seq": {"type": "integer"}, "rebase": {"type": "boolean"}, @@ -42,6 +42,14 @@ "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", diff --git a/lexicons/com/atproto/temp/upgradeRepoVersion.json b/lexicons/com/atproto/temp/upgradeRepoVersion.json new file mode 100644 index 00000000000..bd19374a859 --- /dev/null +++ b/lexicons/com/atproto/temp/upgradeRepoVersion.json @@ -0,0 +1,20 @@ +{ + "lexicon": 1, + "id": "com.atproto.temp.upgradeRepoVersion", + "defs": { + "main": { + "type": "procedure", + "description": "Upgrade a repo to v3", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["did"], + "properties": { + "did": { "type": "string", "format": "did" } + } + } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 94bb818d31a..fd8d39a785c 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -18,7 +18,6 @@ import * as ComAtprotoAdminGetModerationReport from './types/com/atproto/admin/g import * as ComAtprotoAdminGetModerationReports from './types/com/atproto/admin/getModerationReports' import * as ComAtprotoAdminGetRecord from './types/com/atproto/admin/getRecord' import * as ComAtprotoAdminGetRepo from './types/com/atproto/admin/getRepo' -import * as ComAtprotoAdminRebaseRepo from './types/com/atproto/admin/rebaseRepo' import * as ComAtprotoAdminResolveModerationReports from './types/com/atproto/admin/resolveModerationReports' import * as ComAtprotoAdminReverseModerationAction from './types/com/atproto/admin/reverseModerationAction' import * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRepos' @@ -40,7 +39,6 @@ import * as ComAtprotoRepoDescribeRepo from './types/com/atproto/repo/describeRe 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 ComAtprotoRepoRebaseRepo from './types/com/atproto/repo/rebaseRepo' import * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef' import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' @@ -63,8 +61,8 @@ import * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/r 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 ComAtprotoSyncGetCommitPath from './types/com/atproto/sync/getCommitPath' 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' @@ -72,6 +70,7 @@ import * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos' import * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate' import * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl' import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos' +import * as ComAtprotoTempUpgradeRepoVersion from './types/com/atproto/temp/upgradeRepoVersion' import * as AppBskyActorDefs from './types/app/bsky/actor/defs' import * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile' @@ -140,7 +139,6 @@ export * as ComAtprotoAdminGetModerationReport from './types/com/atproto/admin/g export * as ComAtprotoAdminGetModerationReports from './types/com/atproto/admin/getModerationReports' export * as ComAtprotoAdminGetRecord from './types/com/atproto/admin/getRecord' export * as ComAtprotoAdminGetRepo from './types/com/atproto/admin/getRepo' -export * as ComAtprotoAdminRebaseRepo from './types/com/atproto/admin/rebaseRepo' export * as ComAtprotoAdminResolveModerationReports from './types/com/atproto/admin/resolveModerationReports' export * as ComAtprotoAdminReverseModerationAction from './types/com/atproto/admin/reverseModerationAction' export * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRepos' @@ -162,7 +160,6 @@ export * as ComAtprotoRepoDescribeRepo from './types/com/atproto/repo/describeRe export * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord' export * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords' export * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord' -export * as ComAtprotoRepoRebaseRepo from './types/com/atproto/repo/rebaseRepo' export * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef' export * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' export * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' @@ -185,8 +182,8 @@ export * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/r export * as ComAtprotoSyncGetBlob from './types/com/atproto/sync/getBlob' export * as ComAtprotoSyncGetBlocks from './types/com/atproto/sync/getBlocks' export * as ComAtprotoSyncGetCheckout from './types/com/atproto/sync/getCheckout' -export * as ComAtprotoSyncGetCommitPath from './types/com/atproto/sync/getCommitPath' export * as ComAtprotoSyncGetHead from './types/com/atproto/sync/getHead' +export * as ComAtprotoSyncGetLatestCommit from './types/com/atproto/sync/getLatestCommit' export * as ComAtprotoSyncGetRecord from './types/com/atproto/sync/getRecord' export * as ComAtprotoSyncGetRepo from './types/com/atproto/sync/getRepo' export * as ComAtprotoSyncListBlobs from './types/com/atproto/sync/listBlobs' @@ -194,6 +191,7 @@ export * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos' export * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate' export * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl' export * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos' +export * as ComAtprotoTempUpgradeRepoVersion from './types/com/atproto/temp/upgradeRepoVersion' export * as AppBskyActorDefs from './types/app/bsky/actor/defs' export * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' export * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile' @@ -318,6 +316,7 @@ export class AtprotoNS { repo: RepoNS server: ServerNS sync: SyncNS + temp: TempNS constructor(service: AtpServiceClient) { this._service = service @@ -328,6 +327,7 @@ export class AtprotoNS { this.repo = new RepoNS(service) this.server = new ServerNS(service) this.sync = new SyncNS(service) + this.temp = new TempNS(service) } } @@ -448,17 +448,6 @@ export class AdminNS { }) } - rebaseRepo( - data?: ComAtprotoAdminRebaseRepo.InputSchema, - opts?: ComAtprotoAdminRebaseRepo.CallOptions, - ): Promise { - return this._service.xrpc - .call('com.atproto.admin.rebaseRepo', opts?.qp, data, opts) - .catch((e) => { - throw ComAtprotoAdminRebaseRepo.toKnownErr(e) - }) - } - resolveModerationReports( data?: ComAtprotoAdminResolveModerationReports.InputSchema, opts?: ComAtprotoAdminResolveModerationReports.CallOptions, @@ -689,17 +678,6 @@ export class RepoNS { }) } - rebaseRepo( - data?: ComAtprotoRepoRebaseRepo.InputSchema, - opts?: ComAtprotoRepoRebaseRepo.CallOptions, - ): Promise { - return this._service.xrpc - .call('com.atproto.repo.rebaseRepo', opts?.qp, data, opts) - .catch((e) => { - throw ComAtprotoRepoRebaseRepo.toKnownErr(e) - }) - } - uploadBlob( data?: ComAtprotoRepoUploadBlob.InputSchema, opts?: ComAtprotoRepoUploadBlob.CallOptions, @@ -936,17 +914,6 @@ export class SyncNS { }) } - getCommitPath( - params?: ComAtprotoSyncGetCommitPath.QueryParams, - opts?: ComAtprotoSyncGetCommitPath.CallOptions, - ): Promise { - return this._service.xrpc - .call('com.atproto.sync.getCommitPath', params, undefined, opts) - .catch((e) => { - throw ComAtprotoSyncGetCommitPath.toKnownErr(e) - }) - } - getHead( params?: ComAtprotoSyncGetHead.QueryParams, opts?: ComAtprotoSyncGetHead.CallOptions, @@ -958,6 +925,17 @@ export class SyncNS { }) } + getLatestCommit( + params?: ComAtprotoSyncGetLatestCommit.QueryParams, + opts?: ComAtprotoSyncGetLatestCommit.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.sync.getLatestCommit', params, undefined, opts) + .catch((e) => { + throw ComAtprotoSyncGetLatestCommit.toKnownErr(e) + }) + } + getRecord( params?: ComAtprotoSyncGetRecord.QueryParams, opts?: ComAtprotoSyncGetRecord.CallOptions, @@ -1025,6 +1003,25 @@ export class SyncNS { } } +export class TempNS { + _service: AtpServiceClient + + constructor(service: AtpServiceClient) { + this._service = service + } + + upgradeRepoVersion( + data?: ComAtprotoTempUpgradeRepoVersion.InputSchema, + opts?: ComAtprotoTempUpgradeRepoVersion.CallOptions, + ): Promise { + return this._service.xrpc + .call('com.atproto.temp.upgradeRepoVersion', opts?.qp, data, opts) + .catch((e) => { + throw ComAtprotoTempUpgradeRepoVersion.toKnownErr(e) + }) + } +} + export class AppNS { _service: AtpServiceClient bsky: BskyNS diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index b5c82ce2f73..6628ead1154 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -1026,44 +1026,6 @@ export const schemaDict = { }, }, }, - ComAtprotoAdminRebaseRepo: { - lexicon: 1, - id: 'com.atproto.admin.rebaseRepo', - defs: { - main: { - type: 'procedure', - description: "Administrative action to rebase an account's repo", - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['repo'], - properties: { - repo: { - type: 'string', - format: 'at-identifier', - description: 'The handle or DID of the repo.', - }, - swapCommit: { - type: 'string', - format: 'cid', - description: - 'Compare and swap with the previous commit by cid.', - }, - }, - }, - }, - errors: [ - { - name: 'InvalidSwap', - }, - { - name: 'ConcurrentWrites', - }, - ], - }, - }, - }, ComAtprotoAdminResolveModerationReports: { lexicon: 1, id: 'com.atproto.admin.resolveModerationReports', @@ -2229,44 +2191,6 @@ export const schemaDict = { }, }, }, - ComAtprotoRepoRebaseRepo: { - lexicon: 1, - id: 'com.atproto.repo.rebaseRepo', - defs: { - main: { - type: 'procedure', - description: 'Simple rebase of repo that deletes history', - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['repo'], - properties: { - repo: { - type: 'string', - format: 'at-identifier', - description: 'The handle or DID of the repo.', - }, - swapCommit: { - type: 'string', - format: 'cid', - description: - 'Compare and swap with the previous commit by cid.', - }, - }, - }, - }, - errors: [ - { - name: 'InvalidSwap', - }, - { - name: 'ConcurrentWrites', - }, - ], - }, - }, - }, ComAtprotoRepoStrongRef: { lexicon: 1, id: 'com.atproto.repo.strongRef', @@ -3069,7 +2993,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Gets the repo state.', + description: 'DEPRECATED - please use com.atproto.sync.getRepo instead', parameters: { type: 'params', required: ['did'], @@ -3079,12 +3003,6 @@ export const schemaDict = { format: 'did', description: 'The DID of the repo.', }, - commit: { - type: 'string', - format: 'cid', - description: - 'The commit to get the checkout from. Defaults to current HEAD.', - }, }, }, output: { @@ -3093,13 +3011,14 @@ export const schemaDict = { }, }, }, - ComAtprotoSyncGetCommitPath: { + ComAtprotoSyncGetHead: { lexicon: 1, - id: 'com.atproto.sync.getCommitPath', + id: 'com.atproto.sync.getHead', defs: { main: { type: 'query', - description: 'Gets the path of repo commits', + description: + 'DEPRECATED - please use com.atproto.sync.getLatestCommit instead', parameters: { type: 'params', required: ['did'], @@ -3109,44 +3028,36 @@ export const schemaDict = { format: 'did', description: 'The DID of the repo.', }, - latest: { - type: 'string', - format: 'cid', - description: 'The most recent commit', - }, - earliest: { - type: 'string', - format: 'cid', - description: 'The earliest commit to start from', - }, }, }, output: { encoding: 'application/json', schema: { type: 'object', - required: ['commits'], + required: ['root'], properties: { - commits: { - type: 'array', - items: { - type: 'string', - format: 'cid', - }, + root: { + type: 'string', + format: 'cid', }, }, }, }, + errors: [ + { + name: 'HeadNotFound', + }, + ], }, }, }, - ComAtprotoSyncGetHead: { + ComAtprotoSyncGetLatestCommit: { lexicon: 1, - id: 'com.atproto.sync.getHead', + id: 'com.atproto.sync.getLatestCommit', defs: { main: { type: 'query', - description: 'Gets the current HEAD CID of a repo.', + description: 'Gets the current commit CID & revision of the repo.', parameters: { type: 'params', required: ['did'], @@ -3162,18 +3073,21 @@ export const schemaDict = { encoding: 'application/json', schema: { type: 'object', - required: ['root'], + required: ['cid', 'rev'], properties: { - root: { + cid: { type: 'string', format: 'cid', }, + rev: { + type: 'string', + }, }, }, }, errors: [ { - name: 'HeadNotFound', + name: 'RepoNotFound', }, ], }, @@ -3222,7 +3136,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Gets the repo state.', + description: + "Gets the did's repo, optionally catching up from a specific revision.", parameters: { type: 'params', required: ['did'], @@ -3232,16 +3147,10 @@ export const schemaDict = { format: 'did', description: 'The DID of the repo.', }, - earliest: { - type: 'string', - format: 'cid', - description: - 'The earliest commit in the commit range (not inclusive)', - }, - latest: { + since: { type: 'string', format: 'cid', - description: 'The latest commit in the commit range (inclusive)', + description: 'The revision of the repo to catch up from.', }, }, }, @@ -3257,7 +3166,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'List blob cids for some range of commits', + description: 'List blob cids since some revision', parameters: { type: 'params', required: ['did'], @@ -3267,15 +3176,19 @@ export const schemaDict = { format: 'did', description: 'The DID of the repo.', }, - latest: { + since: { type: 'string', format: 'cid', - description: 'The most recent commit', + description: 'Optional revision of the repo to list blobs since', }, - earliest: { + limit: { + type: 'integer', + minimum: 1, + maximum: 1000, + default: 500, + }, + cursor: { type: 'string', - format: 'cid', - description: 'The earliest commit to start from', }, }, }, @@ -3285,6 +3198,9 @@ export const schemaDict = { type: 'object', required: ['cids'], properties: { + cursor: { + type: 'string', + }, cids: { type: 'array', items: { @@ -3449,13 +3365,14 @@ export const schemaDict = { 'tooBig', 'repo', 'commit', - 'prev', + 'rev', + 'since', 'blocks', 'ops', 'blobs', 'time', ], - nullable: ['prev'], + nullable: ['prev', 'since'], properties: { seq: { type: 'integer', @@ -3476,6 +3393,14 @@ export const schemaDict = { 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', @@ -3594,6 +3519,29 @@ export const schemaDict = { }, }, }, + ComAtprotoTempUpgradeRepoVersion: { + lexicon: 1, + id: 'com.atproto.temp.upgradeRepoVersion', + defs: { + main: { + type: 'procedure', + description: 'Upgrade a repo to v3', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + }, + }, + }, + }, + }, + }, AppBskyActorDefs: { lexicon: 1, id: 'app.bsky.actor.defs', @@ -6672,7 +6620,6 @@ export const ids = { ComAtprotoAdminGetModerationReports: 'com.atproto.admin.getModerationReports', ComAtprotoAdminGetRecord: 'com.atproto.admin.getRecord', ComAtprotoAdminGetRepo: 'com.atproto.admin.getRepo', - ComAtprotoAdminRebaseRepo: 'com.atproto.admin.rebaseRepo', ComAtprotoAdminResolveModerationReports: 'com.atproto.admin.resolveModerationReports', ComAtprotoAdminReverseModerationAction: @@ -6696,7 +6643,6 @@ export const ids = { ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord', ComAtprotoRepoListRecords: 'com.atproto.repo.listRecords', ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord', - ComAtprotoRepoRebaseRepo: 'com.atproto.repo.rebaseRepo', ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', @@ -6722,8 +6668,8 @@ export const ids = { ComAtprotoSyncGetBlob: 'com.atproto.sync.getBlob', ComAtprotoSyncGetBlocks: 'com.atproto.sync.getBlocks', ComAtprotoSyncGetCheckout: 'com.atproto.sync.getCheckout', - ComAtprotoSyncGetCommitPath: 'com.atproto.sync.getCommitPath', 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', @@ -6731,6 +6677,7 @@ export const ids = { ComAtprotoSyncNotifyOfUpdate: 'com.atproto.sync.notifyOfUpdate', ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl', ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos', + ComAtprotoTempUpgradeRepoVersion: 'com.atproto.temp.upgradeRepoVersion', AppBskyActorDefs: 'app.bsky.actor.defs', AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences', AppBskyActorGetProfile: 'app.bsky.actor.getProfile', diff --git a/packages/api/src/client/types/com/atproto/admin/rebaseRepo.ts b/packages/api/src/client/types/com/atproto/admin/rebaseRepo.ts deleted file mode 100644 index 8b6c397c4d6..00000000000 --- a/packages/api/src/client/types/com/atproto/admin/rebaseRepo.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * GENERATED CODE - DO NOT MODIFY - */ -import { Headers, XRPCError } from '@atproto/xrpc' -import { ValidationResult, BlobRef } from '@atproto/lexicon' -import { isObj, hasProp } from '../../../../util' -import { lexicons } from '../../../../lexicons' -import { CID } from 'multiformats/cid' - -export interface QueryParams {} - -export interface InputSchema { - /** The handle or DID of the repo. */ - repo: string - /** Compare and swap with the previous commit by cid. */ - swapCommit?: string - [k: string]: unknown -} - -export interface CallOptions { - headers?: Headers - qp?: QueryParams - encoding: 'application/json' -} - -export interface Response { - success: boolean - headers: Headers -} - -export class InvalidSwapError extends XRPCError { - constructor(src: XRPCError) { - super(src.status, src.error, src.message, src.headers) - } -} - -export class ConcurrentWritesError extends XRPCError { - constructor(src: XRPCError) { - super(src.status, src.error, src.message, src.headers) - } -} - -export function toKnownErr(e: any) { - if (e instanceof XRPCError) { - if (e.error === 'InvalidSwap') return new InvalidSwapError(e) - if (e.error === 'ConcurrentWrites') return new ConcurrentWritesError(e) - } - return e -} diff --git a/packages/api/src/client/types/com/atproto/sync/getCheckout.ts b/packages/api/src/client/types/com/atproto/sync/getCheckout.ts index 663918d029b..4210840ec1c 100644 --- a/packages/api/src/client/types/com/atproto/sync/getCheckout.ts +++ b/packages/api/src/client/types/com/atproto/sync/getCheckout.ts @@ -10,8 +10,6 @@ import { CID } from 'multiformats/cid' export interface QueryParams { /** The DID of the repo. */ did: string - /** The commit to get the checkout from. Defaults to current HEAD. */ - commit?: string } export type InputSchema = undefined diff --git a/packages/api/src/client/types/com/atproto/sync/getCommitPath.ts b/packages/api/src/client/types/com/atproto/sync/getLatestCommit.ts similarity index 73% rename from packages/api/src/client/types/com/atproto/sync/getCommitPath.ts rename to packages/api/src/client/types/com/atproto/sync/getLatestCommit.ts index 4caa728a9f2..8d98adeec39 100644 --- a/packages/api/src/client/types/com/atproto/sync/getCommitPath.ts +++ b/packages/api/src/client/types/com/atproto/sync/getLatestCommit.ts @@ -10,16 +10,13 @@ import { CID } from 'multiformats/cid' export interface QueryParams { /** The DID of the repo. */ did: string - /** The most recent commit */ - latest?: string - /** The earliest commit to start from */ - earliest?: string } export type InputSchema = undefined export interface OutputSchema { - commits: string[] + cid: string + rev: string [k: string]: unknown } @@ -33,8 +30,15 @@ export interface Response { data: OutputSchema } +export class RepoNotFoundError extends XRPCError { + constructor(src: XRPCError) { + super(src.status, src.error, src.message, src.headers) + } +} + export function toKnownErr(e: any) { if (e instanceof XRPCError) { + if (e.error === 'RepoNotFound') return new RepoNotFoundError(e) } return e } diff --git a/packages/api/src/client/types/com/atproto/sync/getRepo.ts b/packages/api/src/client/types/com/atproto/sync/getRepo.ts index e01970240d5..0a45536779e 100644 --- a/packages/api/src/client/types/com/atproto/sync/getRepo.ts +++ b/packages/api/src/client/types/com/atproto/sync/getRepo.ts @@ -10,10 +10,8 @@ import { CID } from 'multiformats/cid' export interface QueryParams { /** The DID of the repo. */ did: string - /** The earliest commit in the commit range (not inclusive) */ - earliest?: string - /** The latest commit in the commit range (inclusive) */ - latest?: string + /** The revision of the repo to catch up from. */ + since?: string } export type InputSchema = undefined diff --git a/packages/api/src/client/types/com/atproto/sync/listBlobs.ts b/packages/api/src/client/types/com/atproto/sync/listBlobs.ts index b630d4cf930..72ddd99cb2d 100644 --- a/packages/api/src/client/types/com/atproto/sync/listBlobs.ts +++ b/packages/api/src/client/types/com/atproto/sync/listBlobs.ts @@ -10,15 +10,16 @@ import { CID } from 'multiformats/cid' export interface QueryParams { /** The DID of the repo. */ did: string - /** The most recent commit */ - latest?: string - /** The earliest commit to start from */ - earliest?: 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 } diff --git a/packages/api/src/client/types/com/atproto/sync/subscribeRepos.ts b/packages/api/src/client/types/com/atproto/sync/subscribeRepos.ts index 306f5c815f7..f54c8d45631 100644 --- a/packages/api/src/client/types/com/atproto/sync/subscribeRepos.ts +++ b/packages/api/src/client/types/com/atproto/sync/subscribeRepos.ts @@ -13,7 +13,11 @@ export interface Commit { tooBig: boolean repo: string commit: CID - prev: CID | null + 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[] diff --git a/packages/api/src/client/types/com/atproto/repo/rebaseRepo.ts b/packages/api/src/client/types/com/atproto/temp/upgradeRepoVersion.ts similarity index 52% rename from packages/api/src/client/types/com/atproto/repo/rebaseRepo.ts rename to packages/api/src/client/types/com/atproto/temp/upgradeRepoVersion.ts index 8b6c397c4d6..b8b5aa511b8 100644 --- a/packages/api/src/client/types/com/atproto/repo/rebaseRepo.ts +++ b/packages/api/src/client/types/com/atproto/temp/upgradeRepoVersion.ts @@ -10,10 +10,7 @@ import { CID } from 'multiformats/cid' export interface QueryParams {} export interface InputSchema { - /** The handle or DID of the repo. */ - repo: string - /** Compare and swap with the previous commit by cid. */ - swapCommit?: string + did: string [k: string]: unknown } @@ -28,22 +25,8 @@ export interface Response { headers: Headers } -export class InvalidSwapError extends XRPCError { - constructor(src: XRPCError) { - super(src.status, src.error, src.message, src.headers) - } -} - -export class ConcurrentWritesError extends XRPCError { - constructor(src: XRPCError) { - super(src.status, src.error, src.message, src.headers) - } -} - export function toKnownErr(e: any) { if (e instanceof XRPCError) { - if (e.error === 'InvalidSwap') return new InvalidSwapError(e) - if (e.error === 'ConcurrentWrites') return new ConcurrentWritesError(e) } return e } diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 7143c1833a4..214c5484dc7 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -19,7 +19,6 @@ import * as ComAtprotoAdminGetModerationReport from './types/com/atproto/admin/g import * as ComAtprotoAdminGetModerationReports from './types/com/atproto/admin/getModerationReports' import * as ComAtprotoAdminGetRecord from './types/com/atproto/admin/getRecord' import * as ComAtprotoAdminGetRepo from './types/com/atproto/admin/getRepo' -import * as ComAtprotoAdminRebaseRepo from './types/com/atproto/admin/rebaseRepo' import * as ComAtprotoAdminResolveModerationReports from './types/com/atproto/admin/resolveModerationReports' import * as ComAtprotoAdminReverseModerationAction from './types/com/atproto/admin/reverseModerationAction' import * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRepos' @@ -39,7 +38,6 @@ import * as ComAtprotoRepoDescribeRepo from './types/com/atproto/repo/describeRe 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 ComAtprotoRepoRebaseRepo from './types/com/atproto/repo/rebaseRepo' import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' import * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword' @@ -60,8 +58,8 @@ import * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/r 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 ComAtprotoSyncGetCommitPath from './types/com/atproto/sync/getCommitPath' 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' @@ -69,6 +67,7 @@ import * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos' import * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate' import * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl' import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos' +import * as ComAtprotoTempUpgradeRepoVersion from './types/com/atproto/temp/upgradeRepoVersion' import * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile' import * as AppBskyActorGetProfiles from './types/app/bsky/actor/getProfiles' @@ -162,6 +161,7 @@ export class AtprotoNS { repo: RepoNS server: ServerNS sync: SyncNS + temp: TempNS constructor(server: Server) { this._server = server @@ -172,6 +172,7 @@ export class AtprotoNS { this.repo = new RepoNS(server) this.server = new ServerNS(server) this.sync = new SyncNS(server) + this.temp = new TempNS(server) } } @@ -292,17 +293,6 @@ export class AdminNS { return this._server.xrpc.method(nsid, cfg) } - rebaseRepo( - cfg: ConfigOf< - AV, - ComAtprotoAdminRebaseRepo.Handler>, - ComAtprotoAdminRebaseRepo.HandlerReqCtx> - >, - ) { - const nsid = 'com.atproto.admin.rebaseRepo' // @ts-ignore - return this._server.xrpc.method(nsid, cfg) - } - resolveModerationReports( cfg: ConfigOf< AV, @@ -544,17 +534,6 @@ export class RepoNS { return this._server.xrpc.method(nsid, cfg) } - rebaseRepo( - cfg: ConfigOf< - AV, - ComAtprotoRepoRebaseRepo.Handler>, - ComAtprotoRepoRebaseRepo.HandlerReqCtx> - >, - ) { - const nsid = 'com.atproto.repo.rebaseRepo' // @ts-ignore - return this._server.xrpc.method(nsid, cfg) - } - uploadBlob( cfg: ConfigOf< AV, @@ -791,25 +770,25 @@ export class SyncNS { return this._server.xrpc.method(nsid, cfg) } - getCommitPath( + getHead( cfg: ConfigOf< AV, - ComAtprotoSyncGetCommitPath.Handler>, - ComAtprotoSyncGetCommitPath.HandlerReqCtx> + ComAtprotoSyncGetHead.Handler>, + ComAtprotoSyncGetHead.HandlerReqCtx> >, ) { - const nsid = 'com.atproto.sync.getCommitPath' // @ts-ignore + const nsid = 'com.atproto.sync.getHead' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } - getHead( + getLatestCommit( cfg: ConfigOf< AV, - ComAtprotoSyncGetHead.Handler>, - ComAtprotoSyncGetHead.HandlerReqCtx> + ComAtprotoSyncGetLatestCommit.Handler>, + ComAtprotoSyncGetLatestCommit.HandlerReqCtx> >, ) { - const nsid = 'com.atproto.sync.getHead' // @ts-ignore + const nsid = 'com.atproto.sync.getLatestCommit' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } @@ -891,6 +870,25 @@ export class SyncNS { } } +export class TempNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + upgradeRepoVersion( + cfg: ConfigOf< + AV, + ComAtprotoTempUpgradeRepoVersion.Handler>, + ComAtprotoTempUpgradeRepoVersion.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.temp.upgradeRepoVersion' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + export class AppNS { _server: Server bsky: BskyNS diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index b5c82ce2f73..6628ead1154 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -1026,44 +1026,6 @@ export const schemaDict = { }, }, }, - ComAtprotoAdminRebaseRepo: { - lexicon: 1, - id: 'com.atproto.admin.rebaseRepo', - defs: { - main: { - type: 'procedure', - description: "Administrative action to rebase an account's repo", - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['repo'], - properties: { - repo: { - type: 'string', - format: 'at-identifier', - description: 'The handle or DID of the repo.', - }, - swapCommit: { - type: 'string', - format: 'cid', - description: - 'Compare and swap with the previous commit by cid.', - }, - }, - }, - }, - errors: [ - { - name: 'InvalidSwap', - }, - { - name: 'ConcurrentWrites', - }, - ], - }, - }, - }, ComAtprotoAdminResolveModerationReports: { lexicon: 1, id: 'com.atproto.admin.resolveModerationReports', @@ -2229,44 +2191,6 @@ export const schemaDict = { }, }, }, - ComAtprotoRepoRebaseRepo: { - lexicon: 1, - id: 'com.atproto.repo.rebaseRepo', - defs: { - main: { - type: 'procedure', - description: 'Simple rebase of repo that deletes history', - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['repo'], - properties: { - repo: { - type: 'string', - format: 'at-identifier', - description: 'The handle or DID of the repo.', - }, - swapCommit: { - type: 'string', - format: 'cid', - description: - 'Compare and swap with the previous commit by cid.', - }, - }, - }, - }, - errors: [ - { - name: 'InvalidSwap', - }, - { - name: 'ConcurrentWrites', - }, - ], - }, - }, - }, ComAtprotoRepoStrongRef: { lexicon: 1, id: 'com.atproto.repo.strongRef', @@ -3069,7 +2993,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Gets the repo state.', + description: 'DEPRECATED - please use com.atproto.sync.getRepo instead', parameters: { type: 'params', required: ['did'], @@ -3079,12 +3003,6 @@ export const schemaDict = { format: 'did', description: 'The DID of the repo.', }, - commit: { - type: 'string', - format: 'cid', - description: - 'The commit to get the checkout from. Defaults to current HEAD.', - }, }, }, output: { @@ -3093,13 +3011,14 @@ export const schemaDict = { }, }, }, - ComAtprotoSyncGetCommitPath: { + ComAtprotoSyncGetHead: { lexicon: 1, - id: 'com.atproto.sync.getCommitPath', + id: 'com.atproto.sync.getHead', defs: { main: { type: 'query', - description: 'Gets the path of repo commits', + description: + 'DEPRECATED - please use com.atproto.sync.getLatestCommit instead', parameters: { type: 'params', required: ['did'], @@ -3109,44 +3028,36 @@ export const schemaDict = { format: 'did', description: 'The DID of the repo.', }, - latest: { - type: 'string', - format: 'cid', - description: 'The most recent commit', - }, - earliest: { - type: 'string', - format: 'cid', - description: 'The earliest commit to start from', - }, }, }, output: { encoding: 'application/json', schema: { type: 'object', - required: ['commits'], + required: ['root'], properties: { - commits: { - type: 'array', - items: { - type: 'string', - format: 'cid', - }, + root: { + type: 'string', + format: 'cid', }, }, }, }, + errors: [ + { + name: 'HeadNotFound', + }, + ], }, }, }, - ComAtprotoSyncGetHead: { + ComAtprotoSyncGetLatestCommit: { lexicon: 1, - id: 'com.atproto.sync.getHead', + id: 'com.atproto.sync.getLatestCommit', defs: { main: { type: 'query', - description: 'Gets the current HEAD CID of a repo.', + description: 'Gets the current commit CID & revision of the repo.', parameters: { type: 'params', required: ['did'], @@ -3162,18 +3073,21 @@ export const schemaDict = { encoding: 'application/json', schema: { type: 'object', - required: ['root'], + required: ['cid', 'rev'], properties: { - root: { + cid: { type: 'string', format: 'cid', }, + rev: { + type: 'string', + }, }, }, }, errors: [ { - name: 'HeadNotFound', + name: 'RepoNotFound', }, ], }, @@ -3222,7 +3136,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Gets the repo state.', + description: + "Gets the did's repo, optionally catching up from a specific revision.", parameters: { type: 'params', required: ['did'], @@ -3232,16 +3147,10 @@ export const schemaDict = { format: 'did', description: 'The DID of the repo.', }, - earliest: { - type: 'string', - format: 'cid', - description: - 'The earliest commit in the commit range (not inclusive)', - }, - latest: { + since: { type: 'string', format: 'cid', - description: 'The latest commit in the commit range (inclusive)', + description: 'The revision of the repo to catch up from.', }, }, }, @@ -3257,7 +3166,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'List blob cids for some range of commits', + description: 'List blob cids since some revision', parameters: { type: 'params', required: ['did'], @@ -3267,15 +3176,19 @@ export const schemaDict = { format: 'did', description: 'The DID of the repo.', }, - latest: { + since: { type: 'string', format: 'cid', - description: 'The most recent commit', + description: 'Optional revision of the repo to list blobs since', }, - earliest: { + limit: { + type: 'integer', + minimum: 1, + maximum: 1000, + default: 500, + }, + cursor: { type: 'string', - format: 'cid', - description: 'The earliest commit to start from', }, }, }, @@ -3285,6 +3198,9 @@ export const schemaDict = { type: 'object', required: ['cids'], properties: { + cursor: { + type: 'string', + }, cids: { type: 'array', items: { @@ -3449,13 +3365,14 @@ export const schemaDict = { 'tooBig', 'repo', 'commit', - 'prev', + 'rev', + 'since', 'blocks', 'ops', 'blobs', 'time', ], - nullable: ['prev'], + nullable: ['prev', 'since'], properties: { seq: { type: 'integer', @@ -3476,6 +3393,14 @@ export const schemaDict = { 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', @@ -3594,6 +3519,29 @@ export const schemaDict = { }, }, }, + ComAtprotoTempUpgradeRepoVersion: { + lexicon: 1, + id: 'com.atproto.temp.upgradeRepoVersion', + defs: { + main: { + type: 'procedure', + description: 'Upgrade a repo to v3', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + }, + }, + }, + }, + }, + }, AppBskyActorDefs: { lexicon: 1, id: 'app.bsky.actor.defs', @@ -6672,7 +6620,6 @@ export const ids = { ComAtprotoAdminGetModerationReports: 'com.atproto.admin.getModerationReports', ComAtprotoAdminGetRecord: 'com.atproto.admin.getRecord', ComAtprotoAdminGetRepo: 'com.atproto.admin.getRepo', - ComAtprotoAdminRebaseRepo: 'com.atproto.admin.rebaseRepo', ComAtprotoAdminResolveModerationReports: 'com.atproto.admin.resolveModerationReports', ComAtprotoAdminReverseModerationAction: @@ -6696,7 +6643,6 @@ export const ids = { ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord', ComAtprotoRepoListRecords: 'com.atproto.repo.listRecords', ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord', - ComAtprotoRepoRebaseRepo: 'com.atproto.repo.rebaseRepo', ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', @@ -6722,8 +6668,8 @@ export const ids = { ComAtprotoSyncGetBlob: 'com.atproto.sync.getBlob', ComAtprotoSyncGetBlocks: 'com.atproto.sync.getBlocks', ComAtprotoSyncGetCheckout: 'com.atproto.sync.getCheckout', - ComAtprotoSyncGetCommitPath: 'com.atproto.sync.getCommitPath', 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', @@ -6731,6 +6677,7 @@ export const ids = { ComAtprotoSyncNotifyOfUpdate: 'com.atproto.sync.notifyOfUpdate', ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl', ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos', + ComAtprotoTempUpgradeRepoVersion: 'com.atproto.temp.upgradeRepoVersion', AppBskyActorDefs: 'app.bsky.actor.defs', AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences', AppBskyActorGetProfile: 'app.bsky.actor.getProfile', diff --git a/packages/bsky/src/lexicon/types/com/atproto/repo/rebaseRepo.ts b/packages/bsky/src/lexicon/types/com/atproto/repo/rebaseRepo.ts deleted file mode 100644 index d2fe04b1386..00000000000 --- a/packages/bsky/src/lexicon/types/com/atproto/repo/rebaseRepo.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * GENERATED CODE - DO NOT MODIFY - */ -import express from 'express' -import { ValidationResult, BlobRef } from '@atproto/lexicon' -import { lexicons } from '../../../../lexicons' -import { isObj, hasProp } from '../../../../util' -import { CID } from 'multiformats/cid' -import { HandlerAuth } from '@atproto/xrpc-server' - -export interface QueryParams {} - -export interface InputSchema { - /** The handle or DID of the repo. */ - repo: 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' | 'ConcurrentWrites' -} - -export type HandlerOutput = HandlerError | void -export type HandlerReqCtx = { - auth: HA - params: QueryParams - input: HandlerInput - req: express.Request - res: express.Response -} -export type Handler = ( - ctx: HandlerReqCtx, -) => Promise | HandlerOutput diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/getCheckout.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/getCheckout.ts index 5de2cbfa397..63a657e56b9 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getCheckout.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getCheckout.ts @@ -12,8 +12,6 @@ import { HandlerAuth } from '@atproto/xrpc-server' export interface QueryParams { /** The DID of the repo. */ did: string - /** The commit to get the checkout from. Defaults to current HEAD. */ - commit?: string } export type InputSchema = undefined diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/getCommitPath.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/getLatestCommit.ts similarity index 89% rename from packages/pds/src/lexicon/types/com/atproto/sync/getCommitPath.ts rename to packages/bsky/src/lexicon/types/com/atproto/sync/getLatestCommit.ts index 4bba5b45362..9b91e878724 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/getCommitPath.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getLatestCommit.ts @@ -11,16 +11,13 @@ import { HandlerAuth } from '@atproto/xrpc-server' export interface QueryParams { /** The DID of the repo. */ did: string - /** The most recent commit */ - latest?: string - /** The earliest commit to start from */ - earliest?: string } export type InputSchema = undefined export interface OutputSchema { - commits: string[] + cid: string + rev: string [k: string]: unknown } @@ -35,6 +32,7 @@ export interface HandlerSuccess { export interface HandlerError { status: number message?: string + error?: 'RepoNotFound' } export type HandlerOutput = HandlerError | HandlerSuccess diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/getRepo.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/getRepo.ts index d871a63ed9e..495d31a1a22 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getRepo.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/getRepo.ts @@ -12,10 +12,8 @@ import { HandlerAuth } from '@atproto/xrpc-server' export interface QueryParams { /** The DID of the repo. */ did: string - /** The earliest commit in the commit range (not inclusive) */ - earliest?: string - /** The latest commit in the commit range (inclusive) */ - latest?: string + /** The revision of the repo to catch up from. */ + since?: string } export type InputSchema = undefined diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/listBlobs.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/listBlobs.ts index 49dc1a573d1..936b08a69f8 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/listBlobs.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/listBlobs.ts @@ -11,15 +11,16 @@ import { HandlerAuth } from '@atproto/xrpc-server' export interface QueryParams { /** The DID of the repo. */ did: string - /** The most recent commit */ - latest?: string - /** The earliest commit to start from */ - earliest?: 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 } diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/subscribeRepos.ts b/packages/bsky/src/lexicon/types/com/atproto/sync/subscribeRepos.ts index 7fafb749398..ae9cf01f8f2 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/subscribeRepos.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/sync/subscribeRepos.ts @@ -38,7 +38,11 @@ export interface Commit { tooBig: boolean repo: string commit: CID - prev: CID | null + 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[] diff --git a/packages/pds/src/lexicon/types/com/atproto/admin/rebaseRepo.ts b/packages/bsky/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts similarity index 83% rename from packages/pds/src/lexicon/types/com/atproto/admin/rebaseRepo.ts rename to packages/bsky/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts index d2fe04b1386..13e68eb5c7d 100644 --- a/packages/pds/src/lexicon/types/com/atproto/admin/rebaseRepo.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts @@ -11,10 +11,7 @@ import { HandlerAuth } from '@atproto/xrpc-server' export interface QueryParams {} export interface InputSchema { - /** The handle or DID of the repo. */ - repo: string - /** Compare and swap with the previous commit by cid. */ - swapCommit?: string + did: string [k: string]: unknown } @@ -26,7 +23,6 @@ export interface HandlerInput { export interface HandlerError { status: number message?: string - error?: 'InvalidSwap' | 'ConcurrentWrites' } export type HandlerOutput = HandlerError | void diff --git a/packages/bsky/src/services/indexing/index.ts b/packages/bsky/src/services/indexing/index.ts index 025686d90ae..2af633cce29 100644 --- a/packages/bsky/src/services/indexing/index.ts +++ b/packages/bsky/src/services/indexing/index.ts @@ -1,13 +1,12 @@ import { sql } from 'kysely' import { CID } from 'multiformats/cid' -import AtpAgent, { ComAtprotoSyncGetHead } from '@atproto/api' +import AtpAgent, { ComAtprotoSyncGetLatestCommit } from '@atproto/api' import { - MemoryBlockstore, readCarWithRoot, WriteOpAction, - verifyCheckoutWithCids, - RepoContentsWithCids, + verifyRepo, Commit, + VerifiedRepo, } from '@atproto/repo' import { AtUri } from '@atproto/syntax' import { IdResolver, getPds } from '@atproto/identity' @@ -178,20 +177,14 @@ export class IndexingService { const { api } = new AtpAgent({ service: pds }) const { data: car } = await retryHttp(() => - api.com.atproto.sync.getCheckout({ did, commit }), + api.com.atproto.sync.getRepo({ did }), ) const { root, blocks } = await readCarWithRoot(car) - const storage = new MemoryBlockstore(blocks) - const checkout = await verifyCheckoutWithCids( - storage, - root, - did, - signingKey, - ) + const verifiedRepo = await verifyRepo(blocks, root, did, signingKey) const currRecords = await this.getCurrentRecords(did) - const checkoutRecords = formatCheckout(did, checkout.contents) - const diff = findDiffFromCheckout(currRecords, checkoutRecords) + const repoRecords = formatCheckout(did, verifiedRepo) + const diff = findDiffFromCheckout(currRecords, repoRecords) await Promise.all( diff.map(async (op) => { @@ -305,10 +298,10 @@ export class IndexingService { if (!pds) return false const { api } = new AtpAgent({ service: pds }) try { - await retryHttp(() => api.com.atproto.sync.getHead({ did })) + await retryHttp(() => api.com.atproto.sync.getLatestCommit({ did })) return true } catch (err) { - if (err instanceof ComAtprotoSyncGetHead.HeadNotFoundError) { + if (err instanceof ComAtprotoSyncGetLatestCommit.RepoNotFoundError) { return false } return null @@ -418,18 +411,15 @@ const findDiffFromCheckout = ( const formatCheckout = ( did: string, - contents: RepoContentsWithCids, + verifiedRepo: VerifiedRepo, ): Record => { const records: Record = {} - for (const collection of Object.keys(contents)) { - for (const rkey of Object.keys(contents[collection])) { - const uri = AtUri.make(did, collection, rkey) - const { cid, value } = contents[collection][rkey] - records[uri.toString()] = { - uri, - cid, - value, - } + for (const create of verifiedRepo.creates) { + const uri = AtUri.make(did, create.collection, create.rkey) + records[uri.toString()] = { + uri, + cid: create.cid, + value: create.record, } } return records diff --git a/packages/bsky/tests/indexing.test.ts b/packages/bsky/tests/indexing.test.ts index 10913694dfe..cee3ed5a768 100644 --- a/packages/bsky/tests/indexing.test.ts +++ b/packages/bsky/tests/indexing.test.ts @@ -426,10 +426,11 @@ describe('indexing', () => { { headers: await network.serviceHeaders(sc.dids.alice) }, ) // Index - const { data: head } = await pdsAgent.api.com.atproto.sync.getHead({ - did: sc.dids.alice, - }) - await services.indexing(db).indexRepo(sc.dids.alice, head.root) + const { data: commit } = + await pdsAgent.api.com.atproto.sync.getLatestCommit({ + did: sc.dids.alice, + }) + await services.indexing(db).indexRepo(sc.dids.alice, commit.cid) await network.bsky.processAll() // Check const { data: profile } = await agent.api.app.bsky.actor.getProfile( @@ -470,10 +471,11 @@ describe('indexing', () => { sc.getHeaders(sc.dids.alice), ) // Index - const { data: head } = await pdsAgent.api.com.atproto.sync.getHead({ - did: sc.dids.alice, - }) - await services.indexing(db).indexRepo(sc.dids.alice, head.root) + const { data: commit } = + await pdsAgent.api.com.atproto.sync.getLatestCommit({ + did: sc.dids.alice, + }) + await services.indexing(db).indexRepo(sc.dids.alice, commit.cid) await network.bsky.processAll() // Check const { data: profile } = await agent.api.app.bsky.actor.getProfile( @@ -516,10 +518,11 @@ describe('indexing', () => { .repo(pdsDb) .processWrites({ did: sc.dids.alice, writes }, 1) // Index - const { data: head } = await pdsAgent.api.com.atproto.sync.getHead({ - did: sc.dids.alice, - }) - await services.indexing(db).indexRepo(sc.dids.alice, head.root) + const { data: commit } = + await pdsAgent.api.com.atproto.sync.getLatestCommit({ + did: sc.dids.alice, + }) + await services.indexing(db).indexRepo(sc.dids.alice, commit.cid) // Check const getGoodPost = agent.api.app.bsky.feed.getPostThread( { uri: writes[0].uri.toString(), depth: 0 }, diff --git a/packages/pds/src/api/com/atproto/admin/index.ts b/packages/pds/src/api/com/atproto/admin/index.ts index 78524abdb10..84d1fe3218a 100644 --- a/packages/pds/src/api/com/atproto/admin/index.ts +++ b/packages/pds/src/api/com/atproto/admin/index.ts @@ -16,7 +16,6 @@ import disableInviteCodes from './disableInviteCodes' import getInviteCodes from './getInviteCodes' import updateAccountHandle from './updateAccountHandle' import updateAccountEmail from './updateAccountEmail' -import rebaseRepo from './rebaseRepo' import sendEmail from './sendEmail' export default function (server: Server, ctx: AppContext) { @@ -36,6 +35,5 @@ export default function (server: Server, ctx: AppContext) { getInviteCodes(server, ctx) updateAccountHandle(server, ctx) updateAccountEmail(server, ctx) - rebaseRepo(server, ctx) sendEmail(server, ctx) } diff --git a/packages/pds/src/api/com/atproto/admin/rebaseRepo.ts b/packages/pds/src/api/com/atproto/admin/rebaseRepo.ts deleted file mode 100644 index bbdf61bc7a9..00000000000 --- a/packages/pds/src/api/com/atproto/admin/rebaseRepo.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import { CID } from 'multiformats/cid' -import { BadCommitSwapError } from '../../../../repo' -import { ConcurrentWriteError } from '../../../../services/repo' - -export default function (server: Server, ctx: AppContext) { - server.com.atproto.admin.rebaseRepo({ - auth: ctx.roleVerifier, - handler: async ({ input, auth }) => { - if (!auth.credentials.admin) { - throw new AuthRequiredError('Insufficient privileges') - } - const { repo, swapCommit } = input.body - const swapCommitCid = swapCommit ? CID.parse(swapCommit) : undefined - try { - await ctx.services.repo(ctx.db).rebaseRepo(repo, swapCommitCid) - } catch (err) { - if (err instanceof BadCommitSwapError) { - throw new InvalidRequestError(err.message, 'InvalidSwap') - } else if (err instanceof ConcurrentWriteError) { - throw new InvalidRequestError(err.message, 'ConcurrentWrites') - } - throw err - } - }, - }) -} diff --git a/packages/pds/src/api/com/atproto/index.ts b/packages/pds/src/api/com/atproto/index.ts index a5c26c80495..59fca0fd5a9 100644 --- a/packages/pds/src/api/com/atproto/index.ts +++ b/packages/pds/src/api/com/atproto/index.ts @@ -6,6 +6,7 @@ import moderation from './moderation' import repo from './repo' import serverMethods from './server' import sync from './sync' +import upgradeRepoVersion from './upgradeRepoVersion' export default function (server: Server, ctx: AppContext) { admin(server, ctx) @@ -14,4 +15,5 @@ export default function (server: Server, ctx: AppContext) { repo(server, ctx) serverMethods(server, ctx) sync(server, ctx) + upgradeRepoVersion(server, ctx) } diff --git a/packages/pds/src/api/com/atproto/repo/index.ts b/packages/pds/src/api/com/atproto/repo/index.ts index a95a47c9ba7..ce13a10fe15 100644 --- a/packages/pds/src/api/com/atproto/repo/index.ts +++ b/packages/pds/src/api/com/atproto/repo/index.ts @@ -7,7 +7,6 @@ import describeRepo from './describeRepo' import getRecord from './getRecord' import listRecords from './listRecords' import putRecord from './putRecord' -import rebaseRepo from './rebaseRepo' import uploadBlob from './uploadBlob' export default function (server: Server, ctx: AppContext) { @@ -18,6 +17,5 @@ export default function (server: Server, ctx: AppContext) { getRecord(server, ctx) listRecords(server, ctx) putRecord(server, ctx) - rebaseRepo(server, ctx) uploadBlob(server, ctx) } diff --git a/packages/pds/src/api/com/atproto/repo/rebaseRepo.ts b/packages/pds/src/api/com/atproto/repo/rebaseRepo.ts deleted file mode 100644 index e2fbffaa6d8..00000000000 --- a/packages/pds/src/api/com/atproto/repo/rebaseRepo.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { CID } from 'multiformats/cid' -import { InvalidRequestError, AuthRequiredError } from '@atproto/xrpc-server' -import { Server } from '../../../../lexicon' -import { BadCommitSwapError } from '../../../../repo' -import AppContext from '../../../../context' -import { ConcurrentWriteError } from '../../../../services/repo' -import { DAY } from '@atproto/common' - -export default function (server: Server, ctx: AppContext) { - server.com.atproto.repo.rebaseRepo({ - auth: ctx.accessVerifierNotAppPassword, - rateLimit: { - durationMs: DAY, - points: 10, - calcKey: ({ auth }) => auth.credentials.did, - }, - handler: async ({ input, auth }) => { - const { repo, swapCommit } = input.body - const did = await ctx.services.account(ctx.db).getDidForActor(repo) - - if (!did) { - throw new InvalidRequestError(`Could not find repo: ${repo}`) - } else if (did !== auth.credentials.did) { - throw new AuthRequiredError() - } - - const swapCommitCid = swapCommit ? CID.parse(swapCommit) : undefined - - try { - await ctx.services.repo(ctx.db).rebaseRepo(repo, swapCommitCid) - } catch (err) { - if (err instanceof BadCommitSwapError) { - throw new InvalidRequestError(err.message, 'InvalidSwap') - } else if (err instanceof ConcurrentWriteError) { - throw new InvalidRequestError(err.message, 'ConcurrentWrites') - } - throw err - } - }, - }) -} diff --git a/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts b/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts new file mode 100644 index 00000000000..cbde7131c66 --- /dev/null +++ b/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts @@ -0,0 +1,42 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' +import { byteIterableToStream } from '@atproto/common' +import { Server } from '../../../../../lexicon' +import SqlRepoStorage, { + RepoRootNotFoundError, +} from '../../../../../sql-repo-storage' +import AppContext from '../../../../../context' +import { isUserOrAdmin } from '../../../../../auth' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.sync.getCheckout({ + auth: ctx.optionalAccessOrRoleVerifier, + handler: async ({ params, auth }) => { + const { did } = params + // takedown check for anyone other than an admin or the user + if (!isUserOrAdmin(auth, did)) { + const available = await ctx.services + .account(ctx.db) + .isRepoAvailable(did) + if (!available) { + throw new InvalidRequestError(`Could not find repo for DID: ${did}`) + } + } + + const storage = new SqlRepoStorage(ctx.db, did) + let carStream: AsyncIterable + try { + carStream = await storage.getCarStream() + } catch (err) { + if (err instanceof RepoRootNotFoundError) { + throw new InvalidRequestError(`Could not find repo for DID: ${did}`) + } + throw err + } + + return { + encoding: 'application/vnd.ipld.car', + body: byteIterableToStream(carStream), + } + }, + }) +} diff --git a/packages/pds/src/api/com/atproto/sync/getHead.ts b/packages/pds/src/api/com/atproto/sync/deprecated/getHead.ts similarity index 79% rename from packages/pds/src/api/com/atproto/sync/getHead.ts rename to packages/pds/src/api/com/atproto/sync/deprecated/getHead.ts index bc55ba41d74..acde9cebc38 100644 --- a/packages/pds/src/api/com/atproto/sync/getHead.ts +++ b/packages/pds/src/api/com/atproto/sync/deprecated/getHead.ts @@ -1,8 +1,8 @@ import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../lexicon' -import SqlRepoStorage from '../../../../sql-repo-storage' -import AppContext from '../../../../context' -import { isUserOrAdmin } from '../../../../auth' +import { Server } from '../../../../../lexicon' +import SqlRepoStorage from '../../../../../sql-repo-storage' +import AppContext from '../../../../../context' +import { isUserOrAdmin } from '../../../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getHead({ @@ -22,7 +22,7 @@ export default function (server: Server, ctx: AppContext) { } } const storage = new SqlRepoStorage(ctx.db, did) - const root = await storage.getHead() + const root = await storage.getRoot() if (root === null) { throw new InvalidRequestError( `Could not find root for DID: ${did}`, diff --git a/packages/pds/src/api/com/atproto/sync/getBlocks.ts b/packages/pds/src/api/com/atproto/sync/getBlocks.ts index 6e8a6805e69..a4a85355f13 100644 --- a/packages/pds/src/api/com/atproto/sync/getBlocks.ts +++ b/packages/pds/src/api/com/atproto/sync/getBlocks.ts @@ -1,10 +1,10 @@ import { CID } from 'multiformats/cid' import { InvalidRequestError } from '@atproto/xrpc-server' +import { byteIterableToStream } from '@atproto/common' +import { blocksToCarStream } from '@atproto/repo' import { Server } from '../../../../lexicon' import SqlRepoStorage from '../../../../sql-repo-storage' import AppContext from '../../../../context' -import { blocksToCarStream } from '@atproto/repo' -import { byteIterableToStream } from '@atproto/common' import { isUserOrAdmin } from '../../../../auth' export default function (server: Server, ctx: AppContext) { @@ -21,6 +21,7 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Could not find repo for DID: ${did}`) } } + const cids = params.cids.map((c) => CID.parse(c)) const storage = new SqlRepoStorage(ctx.db, did) const got = await storage.getBlocks(cids) diff --git a/packages/pds/src/api/com/atproto/sync/getCommitPath.ts b/packages/pds/src/api/com/atproto/sync/getCommitPath.ts deleted file mode 100644 index 6adbc2b073b..00000000000 --- a/packages/pds/src/api/com/atproto/sync/getCommitPath.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { CID } from 'multiformats/cid' -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Server } from '../../../../lexicon' -import SqlRepoStorage from '../../../../sql-repo-storage' -import AppContext from '../../../../context' -import { isUserOrAdmin } from '../../../../auth' - -export default function (server: Server, ctx: AppContext) { - server.com.atproto.sync.getCommitPath({ - auth: ctx.optionalAccessOrRoleVerifier, - handler: async ({ params, auth }) => { - const { did } = params - // takedown check for anyone other than an admin or the user - if (!isUserOrAdmin(auth, did)) { - const available = await ctx.services - .account(ctx.db) - .isRepoAvailable(did) - if (!available) { - throw new InvalidRequestError(`Could not find root for DID: ${did}`) - } - } - const storage = new SqlRepoStorage(ctx.db, did) - const earliest = params.earliest ? CID.parse(params.earliest) : null - const latest = params.latest - ? CID.parse(params.latest) - : await storage.getHead() - if (latest === null) { - throw new InvalidRequestError(`Could not find root for DID: ${did}`) - } - const commitPath = await storage.getCommitPath(latest, earliest) - if (commitPath === null) { - throw new InvalidRequestError( - `Could not find a valid commit path from ${latest.toString()} to ${earliest?.toString()}`, - ) - } - const commits = commitPath.map((c) => c.toString()) - return { - encoding: 'application/json', - body: { commits }, - } - }, - }) -} diff --git a/packages/pds/src/api/com/atproto/sync/getCheckout.ts b/packages/pds/src/api/com/atproto/sync/getLatestCommit.ts similarity index 56% rename from packages/pds/src/api/com/atproto/sync/getCheckout.ts rename to packages/pds/src/api/com/atproto/sync/getLatestCommit.ts index 1512d24b12d..877db7806f4 100644 --- a/packages/pds/src/api/com/atproto/sync/getCheckout.ts +++ b/packages/pds/src/api/com/atproto/sync/getLatestCommit.ts @@ -1,14 +1,11 @@ -import { CID } from 'multiformats/cid' -import * as repo from '@atproto/repo' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import SqlRepoStorage from '../../../../sql-repo-storage' import AppContext from '../../../../context' -import { byteIterableToStream } from '@atproto/common' import { isUserOrAdmin } from '../../../../auth' export default function (server: Server, ctx: AppContext) { - server.com.atproto.sync.getCheckout({ + server.com.atproto.sync.getLatestCommit({ auth: ctx.optionalAccessOrRoleVerifier, handler: async ({ params, auth }) => { const { did } = params @@ -18,20 +15,23 @@ export default function (server: Server, ctx: AppContext) { .account(ctx.db) .isRepoAvailable(did) if (!available) { - throw new InvalidRequestError(`Could not find root for DID: ${did}`) + throw new InvalidRequestError( + `Could not find root for DID: ${did}`, + 'RepoNotFound', + ) } } const storage = new SqlRepoStorage(ctx.db, did) - const commit = params.commit - ? CID.parse(params.commit) - : await storage.getHead() - if (!commit) { - throw new InvalidRequestError(`Could not find repo for DID: ${did}`) + const root = await storage.getRootDetailed() + if (root === null) { + throw new InvalidRequestError( + `Could not find root for DID: ${did}`, + 'RepoNotFound', + ) } - const checkout = repo.getCheckout(storage, commit) return { - encoding: 'application/vnd.ipld.car', - body: byteIterableToStream(checkout), + encoding: 'application/json', + body: { cid: root.cid.toString(), rev: root.rev }, } }, }) diff --git a/packages/pds/src/api/com/atproto/sync/getRecord.ts b/packages/pds/src/api/com/atproto/sync/getRecord.ts index 9a9690012a5..817d7850cb6 100644 --- a/packages/pds/src/api/com/atproto/sync/getRecord.ts +++ b/packages/pds/src/api/com/atproto/sync/getRecord.ts @@ -24,7 +24,7 @@ export default function (server: Server, ctx: AppContext) { const storage = new SqlRepoStorage(ctx.db, did) const commit = params.commit ? CID.parse(params.commit) - : await storage.getHead() + : await storage.getRoot() if (!commit) { throw new InvalidRequestError(`Could not find repo for DID: ${did}`) } diff --git a/packages/pds/src/api/com/atproto/sync/getRepo.ts b/packages/pds/src/api/com/atproto/sync/getRepo.ts index fc89ffc8402..9037a2a3a9c 100644 --- a/packages/pds/src/api/com/atproto/sync/getRepo.ts +++ b/packages/pds/src/api/com/atproto/sync/getRepo.ts @@ -1,17 +1,17 @@ -import { CID } from 'multiformats/cid' -import * as repo from '@atproto/repo' import { InvalidRequestError } from '@atproto/xrpc-server' +import { byteIterableToStream } from '@atproto/common' import { Server } from '../../../../lexicon' -import SqlRepoStorage from '../../../../sql-repo-storage' +import SqlRepoStorage, { + RepoRootNotFoundError, +} from '../../../../sql-repo-storage' import AppContext from '../../../../context' -import { byteIterableToStream, chunkArray } from '@atproto/common' import { isUserOrAdmin } from '../../../../auth' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getRepo({ auth: ctx.optionalAccessOrRoleVerifier, handler: async ({ params, auth }) => { - const { did } = params + const { did, since } = params // takedown check for anyone other than an admin or the user if (!isUserOrAdmin(auth, did)) { const available = await ctx.services @@ -21,28 +21,17 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Could not find repo for DID: ${did}`) } } - const storage = new SqlRepoStorage(ctx.db, did) - const earliest = params.earliest ? CID.parse(params.earliest) : null - const latest = params.latest - ? CID.parse(params.latest) - : await storage.getHead() - if (latest === null) { - throw new InvalidRequestError(`Could not find repo for DID: ${did}`) - } - const commitPath = await storage.getCommitPath(latest, earliest) - if (commitPath === null) { - throw new InvalidRequestError(`Could not find shared history`) - } - const commitChunks = chunkArray(commitPath, 25) - const carStream = repo.writeCar(latest, async (car) => { - for (const chunk of commitChunks) { - const blocks = await storage.getAllBlocksForCommits(chunk) - for (const block of blocks) { - await car.put({ cid: block.cid, bytes: block.bytes }) - } + const storage = new SqlRepoStorage(ctx.db, did) + let carStream: AsyncIterable + try { + carStream = await storage.getCarStream(since) + } catch (err) { + if (err instanceof RepoRootNotFoundError) { + throw new InvalidRequestError(`Could not find repo for DID: ${did}`) } - }) + throw err + } return { encoding: 'application/vnd.ipld.car', diff --git a/packages/pds/src/api/com/atproto/sync/index.ts b/packages/pds/src/api/com/atproto/sync/index.ts index 5da7090c8da..5931dbad761 100644 --- a/packages/pds/src/api/com/atproto/sync/index.ts +++ b/packages/pds/src/api/com/atproto/sync/index.ts @@ -2,24 +2,24 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import getBlob from './getBlob' import getBlocks from './getBlocks' -import getCheckout from './getCheckout' -import getCommitPath from './getCommitPath' -import getHead from './getHead' +import getLatestCommit from './getLatestCommit' import getRecord from './getRecord' import getRepo from './getRepo' import subscribeRepos from './subscribeRepos' import listBlobs from './listBlobs' import listRepos from './listRepos' +import getCheckout from './deprecated/getCheckout' +import getHead from './deprecated/getHead' export default function (server: Server, ctx: AppContext) { getBlob(server, ctx) getBlocks(server, ctx) - getCheckout(server, ctx) - getCommitPath(server, ctx) - getHead(server, ctx) + getLatestCommit(server, ctx) getRecord(server, ctx) getRepo(server, ctx) subscribeRepos(server, ctx) listBlobs(server, ctx) listRepos(server, ctx) + getCheckout(server, ctx) + getHead(server, ctx) } diff --git a/packages/pds/src/api/com/atproto/sync/listBlobs.ts b/packages/pds/src/api/com/atproto/sync/listBlobs.ts index 910695b2c9d..5beb4d5a0fd 100644 --- a/packages/pds/src/api/com/atproto/sync/listBlobs.ts +++ b/packages/pds/src/api/com/atproto/sync/listBlobs.ts @@ -1,7 +1,5 @@ -import { CID } from 'multiformats/cid' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import SqlRepoStorage from '../../../../sql-repo-storage' import AppContext from '../../../../context' import { isUserOrAdmin } from '../../../../auth' @@ -9,7 +7,7 @@ export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.listBlobs({ auth: ctx.optionalAccessOrRoleVerifier, handler: async ({ params, auth }) => { - const { did } = params + const { did, since, limit, cursor } = params // takedown check for anyone other than an admin or the user if (!isUserOrAdmin(auth, did)) { const available = await ctx.services @@ -19,28 +17,28 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError(`Could not find root for DID: ${did}`) } } - const storage = new SqlRepoStorage(ctx.db, did) - const earliest = params.earliest ? CID.parse(params.earliest) : null - const latest = params.latest - ? CID.parse(params.latest) - : await storage.getHead() - if (latest === null) { - throw new InvalidRequestError(`Could not find root for DID: ${did}`) + + let builder = ctx.db.db + .selectFrom('repo_blob') + .where('did', '=', did) + .select('cid') + .orderBy('cid', 'asc') + .limit(limit) + if (since) { + builder = builder.where('repoRev', '>', since) } - const commitPath = await storage.getCommitPath(latest, earliest) - if (commitPath === null) { - throw new InvalidRequestError( - `Could not find a valid commit path from ${latest.toString()} to ${earliest?.toString()}`, - ) + + if (cursor) { + builder = builder.where('cid', '>', cursor) } - const cids = await ctx.services - .repo(ctx.db) - .blobs.listForCommits(did, commitPath) + + const res = await builder.execute() return { encoding: 'application/json', body: { - cids: cids.map((c) => c.toString()), + cursor: res.at(-1)?.cid, + cids: res.map((row) => row.cid), }, } }, diff --git a/packages/pds/src/api/com/atproto/upgradeRepoVersion.ts b/packages/pds/src/api/com/atproto/upgradeRepoVersion.ts new file mode 100644 index 00000000000..ad018d08089 --- /dev/null +++ b/packages/pds/src/api/com/atproto/upgradeRepoVersion.ts @@ -0,0 +1,117 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' +import { TID, chunkArray } from '@atproto/common' +import { Server } from '../../../lexicon' +import SqlRepoStorage from '../../../sql-repo-storage' +import AppContext from '../../../context' +import { + BlockMap, + CidSet, + DataDiff, + MST, + MemoryBlockstore, + def, + signCommit, +} from '@atproto/repo' +import { CID } from 'multiformats/cid' +import { formatSeqCommit, sequenceEvt } from '../../../sequencer' + +export default function (server: Server, ctx: AppContext) { + server.com.atproto.temp.upgradeRepoVersion({ + auth: ctx.roleVerifier, + handler: async ({ input, auth }) => { + if (!auth.credentials.admin) { + throw new InvalidRequestError('must be admin') + } + const { did } = input.body + + await ctx.db.transaction(async (dbTxn) => { + const storage = new SqlRepoStorage(dbTxn, did) + await storage.lockRepo() + const prevCid = await storage.getRoot() + if (!prevCid) { + throw new InvalidRequestError('Could not find repo') + } + const prev = await storage.readObj(prevCid, def.versionedCommit) + const records = await dbTxn.db + .selectFrom('record') + .select(['collection', 'rkey', 'cid']) + .where('did', '=', did) + .execute() + const memoryStore = new MemoryBlockstore() + let data = await MST.create(memoryStore) + for (const record of records) { + const dataKey = record.collection + '/' + record.rkey + const cid = CID.parse(record.cid) + data = await data.add(dataKey, cid) + } + const dataCid = await data.getPointer() + if (!dataCid.equals(prev.data)) { + throw new InvalidRequestError('Data cid did not match') + } + const recordCids = records.map((r) => r.cid) + const diff = await DataDiff.of(data, null) + const cidsToKeep = [...recordCids, ...diff.newMstBlocks.cids()] + const rev = TID.nextStr(prev.rev) + for (const chunk of chunkArray(cidsToKeep, 500)) { + const cidStrs = chunk.map((c) => c.toString()) + await dbTxn.db + .updateTable('ipld_block') + .set({ repoRev: rev }) + .where('creator', '=', did) + .where('cid', 'in', cidStrs) + .execute() + } + await dbTxn.db + .deleteFrom('ipld_block') + .where('creator', '=', did) + .where((qb) => + qb.where('repoRev', 'is', null).orWhere('repoRev', '!=', rev), + ) + .execute() + await dbTxn.db + .updateTable('repo_blob') + .set({ repoRev: rev }) + .where('did', '=', did) + .execute() + await dbTxn.db + .updateTable('record') + .set({ repoRev: rev }) + .where('did', '=', did) + .execute() + const commit = await signCommit( + { + did, + version: 3, + rev: TID.nextStr(), + prev: prevCid, + data: dataCid, + }, + ctx.repoSigningKey, + ) + const newBlocks = new BlockMap() + const commitCid = await newBlocks.add(commit) + await storage.putMany(newBlocks, rev) + await dbTxn.db + .updateTable('repo_root') + .set({ + root: commitCid.toString(), + rev, + indexedAt: storage.getTimestamp(), + }) + .where('did', '=', did) + .execute() + + const commitData = { + cid: commitCid, + rev, + prev: prevCid, + since: null, + newBlocks, + removedCids: new CidSet(), + } + const seqEvt = await formatSeqCommit(did, commitData, []) + await sequenceEvt(dbTxn, seqEvt) + }) + }, + }) +} diff --git a/packages/pds/src/app-view/api/app/bsky/util/read-after-write.ts b/packages/pds/src/app-view/api/app/bsky/util/read-after-write.ts index 3ce7074ce6f..b09a30cd2a1 100644 --- a/packages/pds/src/app-view/api/app/bsky/util/read-after-write.ts +++ b/packages/pds/src/app-view/api/app/bsky/util/read-after-write.ts @@ -1,4 +1,5 @@ import { Headers } from '@atproto/xrpc' +import { readStickyLogger as log } from '../../../../../logger' import { LocalRecords } from '../../../../../services/local' import AppContext from '../../../../../context' @@ -49,6 +50,7 @@ export const handleReadAfterWrite = async ( lag = withLocal.lag } catch (err) { body = res.data + log.warn({ err, requester }, 'error in read after write munge') } return { encoding: 'application/json', diff --git a/packages/pds/src/db/database-schema.ts b/packages/pds/src/db/database-schema.ts index 47b09e97211..f77df6b21ad 100644 --- a/packages/pds/src/db/database-schema.ts +++ b/packages/pds/src/db/database-schema.ts @@ -9,8 +9,6 @@ import * as refreshToken from './tables/refresh-token' import * as appPassword from './tables/app-password' import * as record from './tables/record' import * as backlink from './tables/backlink' -import * as repoCommitBlock from './tables/repo-commit-block' -import * as repoCommitHistory from './tables/repo-commit-history' import * as ipldBlock from './tables/ipld-block' import * as inviteCode from './tables/invite-code' import * as notification from './tables/user-notification' @@ -39,11 +37,7 @@ export type DatabaseSchemaType = appView.DatabaseSchemaType & didCache.PartialDB & record.PartialDB & backlink.PartialDB & - repoCommitBlock.PartialDB & - repoCommitHistory.PartialDB & ipldBlock.PartialDB & - repoCommitBlock.PartialDB & - repoCommitHistory.PartialDB & inviteCode.PartialDB & notification.PartialDB & blob.PartialDB & diff --git a/packages/pds/src/db/migrations/20230201T200606704Z-repo-sync-data-pt2.ts b/packages/pds/src/db/migrations/20230201T200606704Z-repo-sync-data-pt2.ts index e4d5b93c5ee..4b84ddce071 100644 --- a/packages/pds/src/db/migrations/20230201T200606704Z-repo-sync-data-pt2.ts +++ b/packages/pds/src/db/migrations/20230201T200606704Z-repo-sync-data-pt2.ts @@ -1,141 +1,4 @@ -import { chunkArray } from '@atproto/common' -import { BlockMap, MemoryBlockstore } from '@atproto/repo' -import { Kysely } from 'kysely' -import { CID } from 'multiformats/cid' -import { RepoCommitBlock } from '../tables/repo-commit-block' -import { RepoCommitHistory } from '../tables/repo-commit-history' +// @NOTE This migration was all data and did not involve any schema changes -export async function up(db: Kysely): Promise { - const migrateUser = async (did: string, head: CID, start: CID | null) => { - const userBlocks = await db - .selectFrom('ipld_block') - .innerJoin( - 'ipld_block_creator as creator', - 'creator.cid', - 'ipld_block.cid', - ) - .where('creator.did', '=', did) - .select(['ipld_block.cid as cid', 'ipld_block.content as content']) - .execute() - - const blocks = new BlockMap() - userBlocks.forEach((row) => { - blocks.set(CID.parse(row.cid), row.content) - }) - - const storage = new MigrationStorage(blocks, db) - - const commitData = await storage.getCommits(head, start) - if (!commitData) return - - const commitBlock: RepoCommitBlock[] = [] - const commitHistory: RepoCommitHistory[] = [] - - for (let i = 0; i < commitData.length; i++) { - const commit = commitData[i] - const prev = commitData[i - 1] - commit.blocks.forEach((_bytes, cid) => { - commitBlock.push({ - commit: commit.commit.toString(), - block: cid.toString(), - creator: did, - }) - }) - commitHistory.push({ - commit: commit.commit.toString(), - prev: prev ? prev.commit.toString() : null, - creator: did, - }) - } - const ipldBlockCreators = storage.blocks.entries().map((entry) => ({ - cid: entry.cid.toString(), - did: did, - })) - - const createRepoCommitBlocks = async () => { - for (const batch of chunkArray(commitBlock, 500)) { - await db - .insertInto('repo_commit_block') - .values(batch) - .onConflict((oc) => oc.doNothing()) - .execute() - } - } - const createRepoCommitHistory = async () => { - for (const batch of chunkArray(commitHistory, 500)) { - await db - .insertInto('repo_commit_history') - .values(batch) - .onConflict((oc) => oc.doNothing()) - .execute() - } - } - const createIpldBlockCreators = async () => { - for (const batch of chunkArray(ipldBlockCreators, 500)) { - await db - .insertInto('ipld_block_creator') - .values(batch) - .onConflict((oc) => oc.doNothing()) - .execute() - } - } - - await Promise.all([ - createRepoCommitBlocks(), - createRepoCommitHistory(), - createIpldBlockCreators(), - ]) - } - - const repoHeads = await db.selectFrom('repo_root').selectAll().execute() - const currHeads: Record = {} - for (const row of repoHeads) { - const head = CID.parse(row.root) - await migrateUser(row.did, head, null) - currHeads[row.did] = head - } -} - -export async function down(_db: Kysely): Promise {} - -class MigrationStorage extends MemoryBlockstore { - constructor(public blocks: BlockMap, public db: Kysely) { - super() - } - - async getBytes(cid: CID): Promise { - const got = this.blocks.get(cid) - if (got) return got - const fromDb = await this.db - .selectFrom('ipld_block') - .where('cid', '=', cid.toString()) - .selectAll() - .executeTakeFirst() - if (!fromDb) return null - this.blocks.set(cid, fromDb.content) - return fromDb.content - } - - async has(cid: CID): Promise { - const got = await this.getBytes(cid) - return !!got - } - - async getBlocks(cids: CID[]): Promise<{ blocks: BlockMap; missing: CID[] }> { - const got = this.blocks.getMany(cids) - if (got.missing.length === 0) return got - const fromDb = await this.db - .selectFrom('ipld_block') - .where( - 'cid', - 'in', - got.missing.map((c) => c.toString()), - ) - .selectAll() - .execute() - fromDb.forEach((row) => { - this.blocks.set(CID.parse(row.cid), row.content) - }) - return this.blocks.getMany(cids) - } -} +export async function up(): Promise {} +export async function down(): Promise {} diff --git a/packages/pds/src/db/migrations/20230828T153013575Z-repo-history-rewrite.ts b/packages/pds/src/db/migrations/20230828T153013575Z-repo-history-rewrite.ts new file mode 100644 index 00000000000..368d7cbdbe5 --- /dev/null +++ b/packages/pds/src/db/migrations/20230828T153013575Z-repo-history-rewrite.ts @@ -0,0 +1,62 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema.alterTable('repo_root').addColumn('rev', 'varchar').execute() + await db.schema + .alterTable('ipld_block') + .addColumn('repoRev', 'varchar') + .execute() + await db.schema + .alterTable('repo_blob') + .addColumn('repoRev', 'varchar') + .execute() + await db.schema.alterTable('repo_blob').dropColumn('commit').execute() + + await db.schema + .createIndex('ipld_block_repo_rev_idx') + .on('ipld_block') + .columns(['creator', 'repoRev', 'cid']) + .execute() + + await db.schema + .createIndex('repo_blob_repo_rev_idx') + .on('repo_blob') + .columns(['did', 'repoRev']) + .execute() + + await db.schema.dropTable('repo_commit_history').execute() + await db.schema.dropTable('repo_commit_block').execute() +} + +export async function down(db: Kysely): Promise { + await db.schema + .createTable('repo_commit_block') + .addColumn('commit', 'varchar', (col) => col.notNull()) + .addColumn('block', 'varchar', (col) => col.notNull()) + .addColumn('creator', 'varchar', (col) => col.notNull()) + .addPrimaryKeyConstraint('repo_commit_block_pkey', [ + 'creator', + 'commit', + 'block', + ]) + .execute() + await db.schema + .createTable('repo_commit_history') + .addColumn('commit', 'varchar', (col) => col.notNull()) + .addColumn('prev', 'varchar') + .addColumn('creator', 'varchar', (col) => col.notNull()) + .addPrimaryKeyConstraint('repo_commit_history_pkey', ['creator', 'commit']) + .execute() + + await db.schema.dropIndex('ipld_block_repo_rev_idx').execute() + + await db.schema.dropIndex('repo_blob_repo_rev_idx').execute() + + await db.schema.alterTable('repo_root').dropColumn('rev').execute() + await db.schema.alterTable('ipld_block').dropColumn('repoRev').execute() + await db.schema.alterTable('repo_blob').dropColumn('repoRev').execute() + await db.schema + .alterTable('repo_blob') + .addColumn('commit', 'varchar') + .execute() +} diff --git a/packages/pds/src/db/migrations/index.ts b/packages/pds/src/db/migrations/index.ts index 4f0f49751ab..e7e521e986a 100644 --- a/packages/pds/src/db/migrations/index.ts +++ b/packages/pds/src/db/migrations/index.ts @@ -64,3 +64,4 @@ export * as _20230810T203412859Z from './20230810T203412859Z-action-duration' export * as _20230818T134357818Z from './20230818T134357818Z-runtime-flags' export * as _20230824T182048120Z from './20230824T182048120Z-remove-post-hierarchy' export * as _20230825T142507884Z from './20230825T142507884Z-blob-tempkey-idx' +export * as _20230828T153013575Z from './20230828T153013575Z-repo-history-rewrite' diff --git a/packages/pds/src/db/tables/ipld-block.ts b/packages/pds/src/db/tables/ipld-block.ts index 7d888252156..ce7bd30a51a 100644 --- a/packages/pds/src/db/tables/ipld-block.ts +++ b/packages/pds/src/db/tables/ipld-block.ts @@ -1,6 +1,7 @@ export interface IpldBlock { cid: string creator: string + repoRev: string | null size: number content: Uint8Array } diff --git a/packages/pds/src/db/tables/repo-blob.ts b/packages/pds/src/db/tables/repo-blob.ts index 5d7fd87b41d..a1fed0877e5 100644 --- a/packages/pds/src/db/tables/repo-blob.ts +++ b/packages/pds/src/db/tables/repo-blob.ts @@ -1,7 +1,7 @@ export interface RepoBlob { cid: string recordUri: string - commit: string + repoRev: string | null did: string takedownId: number | null } diff --git a/packages/pds/src/db/tables/repo-commit-block.ts b/packages/pds/src/db/tables/repo-commit-block.ts deleted file mode 100644 index 2493a4a93cd..00000000000 --- a/packages/pds/src/db/tables/repo-commit-block.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface RepoCommitBlock { - commit: string - block: string - creator: string -} - -export const tableName = 'repo_commit_block' - -export type PartialDB = { [tableName]: RepoCommitBlock } diff --git a/packages/pds/src/db/tables/repo-commit-history.ts b/packages/pds/src/db/tables/repo-commit-history.ts deleted file mode 100644 index d92cb21f3f1..00000000000 --- a/packages/pds/src/db/tables/repo-commit-history.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface RepoCommitHistory { - commit: string - prev: string | null - creator: string -} - -export const tableName = 'repo_commit_history' - -export type PartialDB = { [tableName]: RepoCommitHistory } diff --git a/packages/pds/src/db/tables/repo-root.ts b/packages/pds/src/db/tables/repo-root.ts index 723a171308c..6b6c921f380 100644 --- a/packages/pds/src/db/tables/repo-root.ts +++ b/packages/pds/src/db/tables/repo-root.ts @@ -2,6 +2,7 @@ export interface RepoRoot { did: string root: string + rev: string | null indexedAt: string takedownId: number | null } diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 7143c1833a4..214c5484dc7 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -19,7 +19,6 @@ import * as ComAtprotoAdminGetModerationReport from './types/com/atproto/admin/g import * as ComAtprotoAdminGetModerationReports from './types/com/atproto/admin/getModerationReports' import * as ComAtprotoAdminGetRecord from './types/com/atproto/admin/getRecord' import * as ComAtprotoAdminGetRepo from './types/com/atproto/admin/getRepo' -import * as ComAtprotoAdminRebaseRepo from './types/com/atproto/admin/rebaseRepo' import * as ComAtprotoAdminResolveModerationReports from './types/com/atproto/admin/resolveModerationReports' import * as ComAtprotoAdminReverseModerationAction from './types/com/atproto/admin/reverseModerationAction' import * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRepos' @@ -39,7 +38,6 @@ import * as ComAtprotoRepoDescribeRepo from './types/com/atproto/repo/describeRe 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 ComAtprotoRepoRebaseRepo from './types/com/atproto/repo/rebaseRepo' import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob' import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount' import * as ComAtprotoServerCreateAppPassword from './types/com/atproto/server/createAppPassword' @@ -60,8 +58,8 @@ import * as ComAtprotoServerRevokeAppPassword from './types/com/atproto/server/r 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 ComAtprotoSyncGetCommitPath from './types/com/atproto/sync/getCommitPath' 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' @@ -69,6 +67,7 @@ import * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos' import * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate' import * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl' import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos' +import * as ComAtprotoTempUpgradeRepoVersion from './types/com/atproto/temp/upgradeRepoVersion' import * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile' import * as AppBskyActorGetProfiles from './types/app/bsky/actor/getProfiles' @@ -162,6 +161,7 @@ export class AtprotoNS { repo: RepoNS server: ServerNS sync: SyncNS + temp: TempNS constructor(server: Server) { this._server = server @@ -172,6 +172,7 @@ export class AtprotoNS { this.repo = new RepoNS(server) this.server = new ServerNS(server) this.sync = new SyncNS(server) + this.temp = new TempNS(server) } } @@ -292,17 +293,6 @@ export class AdminNS { return this._server.xrpc.method(nsid, cfg) } - rebaseRepo( - cfg: ConfigOf< - AV, - ComAtprotoAdminRebaseRepo.Handler>, - ComAtprotoAdminRebaseRepo.HandlerReqCtx> - >, - ) { - const nsid = 'com.atproto.admin.rebaseRepo' // @ts-ignore - return this._server.xrpc.method(nsid, cfg) - } - resolveModerationReports( cfg: ConfigOf< AV, @@ -544,17 +534,6 @@ export class RepoNS { return this._server.xrpc.method(nsid, cfg) } - rebaseRepo( - cfg: ConfigOf< - AV, - ComAtprotoRepoRebaseRepo.Handler>, - ComAtprotoRepoRebaseRepo.HandlerReqCtx> - >, - ) { - const nsid = 'com.atproto.repo.rebaseRepo' // @ts-ignore - return this._server.xrpc.method(nsid, cfg) - } - uploadBlob( cfg: ConfigOf< AV, @@ -791,25 +770,25 @@ export class SyncNS { return this._server.xrpc.method(nsid, cfg) } - getCommitPath( + getHead( cfg: ConfigOf< AV, - ComAtprotoSyncGetCommitPath.Handler>, - ComAtprotoSyncGetCommitPath.HandlerReqCtx> + ComAtprotoSyncGetHead.Handler>, + ComAtprotoSyncGetHead.HandlerReqCtx> >, ) { - const nsid = 'com.atproto.sync.getCommitPath' // @ts-ignore + const nsid = 'com.atproto.sync.getHead' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } - getHead( + getLatestCommit( cfg: ConfigOf< AV, - ComAtprotoSyncGetHead.Handler>, - ComAtprotoSyncGetHead.HandlerReqCtx> + ComAtprotoSyncGetLatestCommit.Handler>, + ComAtprotoSyncGetLatestCommit.HandlerReqCtx> >, ) { - const nsid = 'com.atproto.sync.getHead' // @ts-ignore + const nsid = 'com.atproto.sync.getLatestCommit' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } @@ -891,6 +870,25 @@ export class SyncNS { } } +export class TempNS { + _server: Server + + constructor(server: Server) { + this._server = server + } + + upgradeRepoVersion( + cfg: ConfigOf< + AV, + ComAtprotoTempUpgradeRepoVersion.Handler>, + ComAtprotoTempUpgradeRepoVersion.HandlerReqCtx> + >, + ) { + const nsid = 'com.atproto.temp.upgradeRepoVersion' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } +} + export class AppNS { _server: Server bsky: BskyNS diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index b5c82ce2f73..6628ead1154 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -1026,44 +1026,6 @@ export const schemaDict = { }, }, }, - ComAtprotoAdminRebaseRepo: { - lexicon: 1, - id: 'com.atproto.admin.rebaseRepo', - defs: { - main: { - type: 'procedure', - description: "Administrative action to rebase an account's repo", - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['repo'], - properties: { - repo: { - type: 'string', - format: 'at-identifier', - description: 'The handle or DID of the repo.', - }, - swapCommit: { - type: 'string', - format: 'cid', - description: - 'Compare and swap with the previous commit by cid.', - }, - }, - }, - }, - errors: [ - { - name: 'InvalidSwap', - }, - { - name: 'ConcurrentWrites', - }, - ], - }, - }, - }, ComAtprotoAdminResolveModerationReports: { lexicon: 1, id: 'com.atproto.admin.resolveModerationReports', @@ -2229,44 +2191,6 @@ export const schemaDict = { }, }, }, - ComAtprotoRepoRebaseRepo: { - lexicon: 1, - id: 'com.atproto.repo.rebaseRepo', - defs: { - main: { - type: 'procedure', - description: 'Simple rebase of repo that deletes history', - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['repo'], - properties: { - repo: { - type: 'string', - format: 'at-identifier', - description: 'The handle or DID of the repo.', - }, - swapCommit: { - type: 'string', - format: 'cid', - description: - 'Compare and swap with the previous commit by cid.', - }, - }, - }, - }, - errors: [ - { - name: 'InvalidSwap', - }, - { - name: 'ConcurrentWrites', - }, - ], - }, - }, - }, ComAtprotoRepoStrongRef: { lexicon: 1, id: 'com.atproto.repo.strongRef', @@ -3069,7 +2993,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Gets the repo state.', + description: 'DEPRECATED - please use com.atproto.sync.getRepo instead', parameters: { type: 'params', required: ['did'], @@ -3079,12 +3003,6 @@ export const schemaDict = { format: 'did', description: 'The DID of the repo.', }, - commit: { - type: 'string', - format: 'cid', - description: - 'The commit to get the checkout from. Defaults to current HEAD.', - }, }, }, output: { @@ -3093,13 +3011,14 @@ export const schemaDict = { }, }, }, - ComAtprotoSyncGetCommitPath: { + ComAtprotoSyncGetHead: { lexicon: 1, - id: 'com.atproto.sync.getCommitPath', + id: 'com.atproto.sync.getHead', defs: { main: { type: 'query', - description: 'Gets the path of repo commits', + description: + 'DEPRECATED - please use com.atproto.sync.getLatestCommit instead', parameters: { type: 'params', required: ['did'], @@ -3109,44 +3028,36 @@ export const schemaDict = { format: 'did', description: 'The DID of the repo.', }, - latest: { - type: 'string', - format: 'cid', - description: 'The most recent commit', - }, - earliest: { - type: 'string', - format: 'cid', - description: 'The earliest commit to start from', - }, }, }, output: { encoding: 'application/json', schema: { type: 'object', - required: ['commits'], + required: ['root'], properties: { - commits: { - type: 'array', - items: { - type: 'string', - format: 'cid', - }, + root: { + type: 'string', + format: 'cid', }, }, }, }, + errors: [ + { + name: 'HeadNotFound', + }, + ], }, }, }, - ComAtprotoSyncGetHead: { + ComAtprotoSyncGetLatestCommit: { lexicon: 1, - id: 'com.atproto.sync.getHead', + id: 'com.atproto.sync.getLatestCommit', defs: { main: { type: 'query', - description: 'Gets the current HEAD CID of a repo.', + description: 'Gets the current commit CID & revision of the repo.', parameters: { type: 'params', required: ['did'], @@ -3162,18 +3073,21 @@ export const schemaDict = { encoding: 'application/json', schema: { type: 'object', - required: ['root'], + required: ['cid', 'rev'], properties: { - root: { + cid: { type: 'string', format: 'cid', }, + rev: { + type: 'string', + }, }, }, }, errors: [ { - name: 'HeadNotFound', + name: 'RepoNotFound', }, ], }, @@ -3222,7 +3136,8 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'Gets the repo state.', + description: + "Gets the did's repo, optionally catching up from a specific revision.", parameters: { type: 'params', required: ['did'], @@ -3232,16 +3147,10 @@ export const schemaDict = { format: 'did', description: 'The DID of the repo.', }, - earliest: { - type: 'string', - format: 'cid', - description: - 'The earliest commit in the commit range (not inclusive)', - }, - latest: { + since: { type: 'string', format: 'cid', - description: 'The latest commit in the commit range (inclusive)', + description: 'The revision of the repo to catch up from.', }, }, }, @@ -3257,7 +3166,7 @@ export const schemaDict = { defs: { main: { type: 'query', - description: 'List blob cids for some range of commits', + description: 'List blob cids since some revision', parameters: { type: 'params', required: ['did'], @@ -3267,15 +3176,19 @@ export const schemaDict = { format: 'did', description: 'The DID of the repo.', }, - latest: { + since: { type: 'string', format: 'cid', - description: 'The most recent commit', + description: 'Optional revision of the repo to list blobs since', }, - earliest: { + limit: { + type: 'integer', + minimum: 1, + maximum: 1000, + default: 500, + }, + cursor: { type: 'string', - format: 'cid', - description: 'The earliest commit to start from', }, }, }, @@ -3285,6 +3198,9 @@ export const schemaDict = { type: 'object', required: ['cids'], properties: { + cursor: { + type: 'string', + }, cids: { type: 'array', items: { @@ -3449,13 +3365,14 @@ export const schemaDict = { 'tooBig', 'repo', 'commit', - 'prev', + 'rev', + 'since', 'blocks', 'ops', 'blobs', 'time', ], - nullable: ['prev'], + nullable: ['prev', 'since'], properties: { seq: { type: 'integer', @@ -3476,6 +3393,14 @@ export const schemaDict = { 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', @@ -3594,6 +3519,29 @@ export const schemaDict = { }, }, }, + ComAtprotoTempUpgradeRepoVersion: { + lexicon: 1, + id: 'com.atproto.temp.upgradeRepoVersion', + defs: { + main: { + type: 'procedure', + description: 'Upgrade a repo to v3', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['did'], + properties: { + did: { + type: 'string', + format: 'did', + }, + }, + }, + }, + }, + }, + }, AppBskyActorDefs: { lexicon: 1, id: 'app.bsky.actor.defs', @@ -6672,7 +6620,6 @@ export const ids = { ComAtprotoAdminGetModerationReports: 'com.atproto.admin.getModerationReports', ComAtprotoAdminGetRecord: 'com.atproto.admin.getRecord', ComAtprotoAdminGetRepo: 'com.atproto.admin.getRepo', - ComAtprotoAdminRebaseRepo: 'com.atproto.admin.rebaseRepo', ComAtprotoAdminResolveModerationReports: 'com.atproto.admin.resolveModerationReports', ComAtprotoAdminReverseModerationAction: @@ -6696,7 +6643,6 @@ export const ids = { ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord', ComAtprotoRepoListRecords: 'com.atproto.repo.listRecords', ComAtprotoRepoPutRecord: 'com.atproto.repo.putRecord', - ComAtprotoRepoRebaseRepo: 'com.atproto.repo.rebaseRepo', ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob', ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount', @@ -6722,8 +6668,8 @@ export const ids = { ComAtprotoSyncGetBlob: 'com.atproto.sync.getBlob', ComAtprotoSyncGetBlocks: 'com.atproto.sync.getBlocks', ComAtprotoSyncGetCheckout: 'com.atproto.sync.getCheckout', - ComAtprotoSyncGetCommitPath: 'com.atproto.sync.getCommitPath', 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', @@ -6731,6 +6677,7 @@ export const ids = { ComAtprotoSyncNotifyOfUpdate: 'com.atproto.sync.notifyOfUpdate', ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl', ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos', + ComAtprotoTempUpgradeRepoVersion: 'com.atproto.temp.upgradeRepoVersion', AppBskyActorDefs: 'app.bsky.actor.defs', AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences', AppBskyActorGetProfile: 'app.bsky.actor.getProfile', diff --git a/packages/pds/src/lexicon/types/com/atproto/repo/rebaseRepo.ts b/packages/pds/src/lexicon/types/com/atproto/repo/rebaseRepo.ts deleted file mode 100644 index d2fe04b1386..00000000000 --- a/packages/pds/src/lexicon/types/com/atproto/repo/rebaseRepo.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * GENERATED CODE - DO NOT MODIFY - */ -import express from 'express' -import { ValidationResult, BlobRef } from '@atproto/lexicon' -import { lexicons } from '../../../../lexicons' -import { isObj, hasProp } from '../../../../util' -import { CID } from 'multiformats/cid' -import { HandlerAuth } from '@atproto/xrpc-server' - -export interface QueryParams {} - -export interface InputSchema { - /** The handle or DID of the repo. */ - repo: 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' | 'ConcurrentWrites' -} - -export type HandlerOutput = HandlerError | void -export type HandlerReqCtx = { - auth: HA - params: QueryParams - input: HandlerInput - req: express.Request - res: express.Response -} -export type Handler = ( - ctx: HandlerReqCtx, -) => Promise | HandlerOutput diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/getCheckout.ts b/packages/pds/src/lexicon/types/com/atproto/sync/getCheckout.ts index 5de2cbfa397..63a657e56b9 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/getCheckout.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/getCheckout.ts @@ -12,8 +12,6 @@ import { HandlerAuth } from '@atproto/xrpc-server' export interface QueryParams { /** The DID of the repo. */ did: string - /** The commit to get the checkout from. Defaults to current HEAD. */ - commit?: string } export type InputSchema = undefined diff --git a/packages/bsky/src/lexicon/types/com/atproto/sync/getCommitPath.ts b/packages/pds/src/lexicon/types/com/atproto/sync/getLatestCommit.ts similarity index 89% rename from packages/bsky/src/lexicon/types/com/atproto/sync/getCommitPath.ts rename to packages/pds/src/lexicon/types/com/atproto/sync/getLatestCommit.ts index 4bba5b45362..9b91e878724 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/sync/getCommitPath.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/getLatestCommit.ts @@ -11,16 +11,13 @@ import { HandlerAuth } from '@atproto/xrpc-server' export interface QueryParams { /** The DID of the repo. */ did: string - /** The most recent commit */ - latest?: string - /** The earliest commit to start from */ - earliest?: string } export type InputSchema = undefined export interface OutputSchema { - commits: string[] + cid: string + rev: string [k: string]: unknown } @@ -35,6 +32,7 @@ export interface HandlerSuccess { export interface HandlerError { status: number message?: string + error?: 'RepoNotFound' } export type HandlerOutput = HandlerError | HandlerSuccess diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/getRepo.ts b/packages/pds/src/lexicon/types/com/atproto/sync/getRepo.ts index d871a63ed9e..495d31a1a22 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/getRepo.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/getRepo.ts @@ -12,10 +12,8 @@ import { HandlerAuth } from '@atproto/xrpc-server' export interface QueryParams { /** The DID of the repo. */ did: string - /** The earliest commit in the commit range (not inclusive) */ - earliest?: string - /** The latest commit in the commit range (inclusive) */ - latest?: string + /** The revision of the repo to catch up from. */ + since?: string } export type InputSchema = undefined diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/listBlobs.ts b/packages/pds/src/lexicon/types/com/atproto/sync/listBlobs.ts index 49dc1a573d1..936b08a69f8 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/listBlobs.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/listBlobs.ts @@ -11,15 +11,16 @@ import { HandlerAuth } from '@atproto/xrpc-server' export interface QueryParams { /** The DID of the repo. */ did: string - /** The most recent commit */ - latest?: string - /** The earliest commit to start from */ - earliest?: 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 } diff --git a/packages/pds/src/lexicon/types/com/atproto/sync/subscribeRepos.ts b/packages/pds/src/lexicon/types/com/atproto/sync/subscribeRepos.ts index 7fafb749398..ae9cf01f8f2 100644 --- a/packages/pds/src/lexicon/types/com/atproto/sync/subscribeRepos.ts +++ b/packages/pds/src/lexicon/types/com/atproto/sync/subscribeRepos.ts @@ -38,7 +38,11 @@ export interface Commit { tooBig: boolean repo: string commit: CID - prev: CID | null + 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[] diff --git a/packages/bsky/src/lexicon/types/com/atproto/admin/rebaseRepo.ts b/packages/pds/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts similarity index 83% rename from packages/bsky/src/lexicon/types/com/atproto/admin/rebaseRepo.ts rename to packages/pds/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts index d2fe04b1386..13e68eb5c7d 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/admin/rebaseRepo.ts +++ b/packages/pds/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts @@ -11,10 +11,7 @@ import { HandlerAuth } from '@atproto/xrpc-server' export interface QueryParams {} export interface InputSchema { - /** The handle or DID of the repo. */ - repo: string - /** Compare and swap with the previous commit by cid. */ - swapCommit?: string + did: string [k: string]: unknown } @@ -26,7 +23,6 @@ export interface HandlerInput { export interface HandlerError { status: number message?: string - error?: 'InvalidSwap' | 'ConcurrentWrites' } export type HandlerOutput = HandlerError | void diff --git a/packages/pds/src/logger.ts b/packages/pds/src/logger.ts index 7a5d77ddbca..a4277e41669 100644 --- a/packages/pds/src/logger.ts +++ b/packages/pds/src/logger.ts @@ -5,6 +5,7 @@ import * as jwt from 'jsonwebtoken' import { parseBasicAuth } from './auth' export const dbLogger = subsystemLogger('pds:db') +export const readStickyLogger = subsystemLogger('pds:read-sticky') export const redisLogger = subsystemLogger('pds:redis') export const seqLogger = subsystemLogger('pds:sequencer') export const mailerLogger = subsystemLogger('pds:mailer') diff --git a/packages/pds/src/sequencer/events.ts b/packages/pds/src/sequencer/events.ts index fc6e98729cd..eb7bbee5b04 100644 --- a/packages/pds/src/sequencer/events.ts +++ b/packages/pds/src/sequencer/events.ts @@ -6,7 +6,6 @@ import { blocksToCarFile, CidSet, CommitData, - RebaseData, WriteOpAction, } from '@atproto/repo' import { PreparedWrite } from '../repo' @@ -50,11 +49,11 @@ export const formatSeqCommit = async ( let carSlice: Uint8Array // max 200 ops or 1MB of data - if (writes.length > 200 || commitData.blocks.byteSize > 1000000) { + if (writes.length > 200 || commitData.newBlocks.byteSize > 1000000) { tooBig = true const justRoot = new BlockMap() - justRoot.add(commitData.blocks.get(commitData.commit)) - carSlice = await blocksToCarFile(commitData.commit, justRoot) + justRoot.add(commitData.newBlocks.get(commitData.cid)) + carSlice = await blocksToCarFile(commitData.cid, justRoot) } else { tooBig = false for (const w of writes) { @@ -70,15 +69,17 @@ export const formatSeqCommit = async ( } ops.push({ action: w.action, path, cid }) } - carSlice = await blocksToCarFile(commitData.commit, commitData.blocks) + carSlice = await blocksToCarFile(commitData.cid, commitData.newBlocks) } const evt: CommitEvt = { rebase: false, tooBig, repo: did, - commit: commitData.commit, + commit: commitData.cid, prev: commitData.prev, + rev: commitData.rev, + since: commitData.since, ops, blocks: carSlice, blobs: blobs.toList(), @@ -91,30 +92,6 @@ export const formatSeqCommit = async ( } } -export const formatSeqRebase = async ( - did: string, - rebaseData: RebaseData, -): Promise => { - const carSlice = await blocksToCarFile(rebaseData.commit, rebaseData.blocks) - - const evt: CommitEvt = { - rebase: true, - tooBig: false, - repo: did, - commit: rebaseData.commit, - prev: rebaseData.rebased, - ops: [], - blocks: carSlice, - blobs: [], - } - return { - did, - eventType: 'rebase', - event: cborEncode(evt), - sequencedAt: new Date().toISOString(), - } -} - export const formatSeqHandleUpdate = async ( did: string, handle: string, @@ -185,6 +162,8 @@ export const commitEvt = z.object({ repo: z.string(), commit: schema.cid, prev: schema.cid.nullable(), + rev: z.string(), + since: z.string().nullable(), blocks: schema.bytes, ops: z.array(commitEvtOp), blobs: z.array(schema.cid), diff --git a/packages/pds/src/services/moderation/index.ts b/packages/pds/src/services/moderation/index.ts index 2f0bcb5415d..0a5ba6d02b0 100644 --- a/packages/pds/src/services/moderation/index.ts +++ b/packages/pds/src/services/moderation/index.ts @@ -600,7 +600,7 @@ export class ModerationService { // Resolve subject info let subjectInfo: SubjectInfo if ('did' in subject) { - const repo = await new SqlRepoStorage(this.db, subject.did).getHead() + const repo = await new SqlRepoStorage(this.db, subject.did).getRoot() if (!repo) throw new InvalidRequestError('Repo not found') subjectInfo = { subjectType: 'com.atproto.admin.defs#repoRef', diff --git a/packages/pds/src/services/repo/blobs.ts b/packages/pds/src/services/repo/blobs.ts index 5f4f757f768..7078f66a76e 100644 --- a/packages/pds/src/services/repo/blobs.ts +++ b/packages/pds/src/services/repo/blobs.ts @@ -60,7 +60,7 @@ export class RepoBlobs { return new BlobRef(cid, mimeType, size) } - async processWriteBlobs(did: string, commit: CID, writes: PreparedWrite[]) { + async processWriteBlobs(did: string, rev: string, writes: PreparedWrite[]) { await this.deleteDereferencedBlobs(did, writes) const blobPromises: Promise[] = [] @@ -71,7 +71,7 @@ export class RepoBlobs { ) { for (const blob of write.blobs) { blobPromises.push(this.verifyBlobAndMakePermanent(did, blob)) - blobPromises.push(this.associateBlob(blob, write.uri, commit, did)) + blobPromises.push(this.associateBlob(blob, write.uri, rev, did)) } } } @@ -186,7 +186,7 @@ export class RepoBlobs { async associateBlob( blob: PreparedBlobRef, recordUri: AtUri, - commit: CID, + repoRev: string, did: string, ): Promise { await this.db.db @@ -194,30 +194,22 @@ export class RepoBlobs { .values({ cid: blob.cid.toString(), recordUri: recordUri.toString(), - commit: commit.toString(), + repoRev, did, }) .onConflict((oc) => oc.doNothing()) .execute() } - async processRebaseBlobs(did: string, newRoot: CID) { - await this.db.db - .updateTable('repo_blob') - .set({ commit: newRoot.toString() }) - .where('did', '=', did) - .execute() - } - - async listForCommits(did: string, commits: CID[]): Promise { - if (commits.length < 1) return [] - const commitStrs = commits.map((c) => c.toString()) - const res = await this.db.db + async listSinceRev(did: string, rev?: string): Promise { + let builder = this.db.db .selectFrom('repo_blob') .where('did', '=', did) - .where('commit', 'in', commitStrs) .select('cid') - .execute() + if (rev) { + builder = builder.where('repoRev', '>', rev) + } + const res = await builder.execute() const cids = res.map((row) => CID.parse(row.cid)) return new CidSet(cids).toList() } diff --git a/packages/pds/src/services/repo/index.ts b/packages/pds/src/services/repo/index.ts index aed7a862a39..449842c9983 100644 --- a/packages/pds/src/services/repo/index.ts +++ b/packages/pds/src/services/repo/index.ts @@ -1,15 +1,8 @@ import { CID } from 'multiformats/cid' import * as crypto from '@atproto/crypto' -import { - BlobStore, - CommitData, - RebaseData, - Repo, - WriteOpAction, -} from '@atproto/repo' -import * as repo from '@atproto/repo' -import { AtUri } from '@atproto/syntax' +import { BlobStore, CommitData, Repo, WriteOpAction } from '@atproto/repo' import { InvalidRequestError } from '@atproto/xrpc-server' +import { AtUri } from '@atproto/syntax' import Database from '../../db' import { MessageQueue } from '../../event-stream/types' import SqlRepoStorage from '../../sql-repo-storage' @@ -26,7 +19,6 @@ import * as sequencer from '../../sequencer' import { Labeler } from '../../labeler' import { wait } from '@atproto/common' import { BackgroundQueue } from '../../event-stream/background-queue' -import { countAll } from '../../db/util' import { Crawlers } from '../../crawlers' import { ContentReporter } from '../../content-reporter' @@ -104,7 +96,7 @@ export class RepoService { await Promise.all([ storage.applyCommit(commit), this.indexWrites(writes, now), - this.blobs.processWriteBlobs(did, commit.commit, writes), + this.blobs.processWriteBlobs(did, commit.rev, writes), ]) await this.afterWriteProcessing(did, commit, writes) } @@ -127,7 +119,7 @@ export class RepoService { // & send to indexing this.indexWrites(writes, now, commitData.rev), // process blobs - this.blobs.processWriteBlobs(did, commitData.commit, writes), + this.blobs.processWriteBlobs(did, commitData.rev, writes), // do any other processing needed after write ]) await this.afterWriteProcessing(did, commitData, writes) @@ -167,20 +159,24 @@ export class RepoService { writes: PreparedWrite[], swapCommit?: CID, ): Promise { - const currRoot = await storage.getHead() + const currRoot = await storage.getRootDetailed() if (!currRoot) { throw new InvalidRequestError( `${did} is not a registered repo on this server`, ) } - if (swapCommit && !currRoot.equals(swapCommit)) { - throw new BadCommitSwapError(currRoot) + if (swapCommit && !currRoot.cid.equals(swapCommit)) { + throw new BadCommitSwapError(currRoot.cid) } // cache last commit since there's likely overlap - await storage.cacheCommit(currRoot) + await storage.cacheRev(currRoot.rev) const recordTxn = this.services.record(this.db) + const delAndUpdateUris: AtUri[] = [] for (const write of writes) { const { action, uri, swapCid } = write + if (action !== WriteOpAction.Create) { + delAndUpdateUris.push(uri) + } if (swapCid === undefined) { continue } @@ -200,8 +196,18 @@ export class RepoService { } } const writeOps = writes.map(writeToOp) - const repo = await Repo.load(storage, currRoot) - return repo.formatCommit(writeOps, this.repoSigningKey) + const repo = await Repo.load(storage, currRoot.cid) + const commit = await repo.formatCommit(writeOps, this.repoSigningKey) + // find blocks that would be deleted but are referenced by another record + const dupeRecordCids = await this.getDuplicateRecordCids( + did, + commit.removedCids.toList(), + delAndUpdateUris, + ) + for (const cid of dupeRecordCids) { + commit.removedCids.delete(cid) + } + return commit } async indexWrites(writes: PreparedWrite[], now: string, rev?: string) { @@ -228,6 +234,26 @@ export class RepoService { ) } + async getDuplicateRecordCids( + did: string, + cids: CID[], + touchedUris: AtUri[], + ): Promise { + if (touchedUris.length === 0 || cids.length === 0) { + return [] + } + const cidStrs = cids.map((c) => c.toString()) + const uriStrs = touchedUris.map((u) => u.toString()) + const res = await this.db.db + .selectFrom('record') + .where('did', '=', did) + .where('cid', 'in', cidStrs) + .where('uri', 'not in', uriStrs) + .select('cid') + .execute() + return res.map((row) => CID.parse(row.cid)) + } + async afterWriteProcessing( did: string, commitData: CommitData, @@ -253,123 +279,12 @@ export class RepoService { await sequencer.sequenceEvt(this.db, seqEvt) } - async rebaseRepo(did: string, swapCommit?: CID) { - this.db.assertNotTransaction() - - // rebases are expensive & should be done rarely, we don't try to re-process on concurrent writes - await this.serviceTx(async (srvcTx) => { - const rebaseData = await srvcTx.formatRebase(did, swapCommit) - await srvcTx.processRebase(did, rebaseData) - }) - } - - async formatRebase(did: string, swapCommit?: CID): Promise { - const storage = new SqlRepoStorage(this.db, did, new Date().toISOString()) - const locked = await storage.lockRepo() - if (!locked) { - throw new ConcurrentWriteError() - } - - const currRoot = await storage.getHead() - if (!currRoot) { - throw new InvalidRequestError( - `${did} is not a registered repo on this server`, - ) - } else if (swapCommit && !currRoot.equals(swapCommit)) { - throw new BadCommitSwapError(currRoot) - } - - const records = await this.db.db - .selectFrom('record') - .where('did', '=', did) - .select(['uri', 'cid']) - .execute() - // this will do everything in memory & shouldn't touch storage until we do .getUnstoredBlocks - let data = await repo.MST.create(storage) - for (const record of records) { - const uri = new AtUri(record.uri) - const cid = CID.parse(record.cid) - const dataKey = repo.formatDataKey(uri.collection, uri.rkey) - data = await data.add(dataKey, cid) - } - // this looks for unstored blocks recursively & bails when it encounters a block it has - // in most cases, there should be no unstored blocks, but this allows for recovery of repos in a broken state - const unstoredData = await data.getUnstoredBlocks() - const commit = await repo.signCommit( - { - did, - version: 2, - prev: null, - data: unstoredData.root, - }, - this.repoSigningKey, - ) - const newBlocks = unstoredData.blocks - const currCids = await data.allCids() - const commitCid = await newBlocks.add(commit) - return { - commit: commitCid, - rebased: currRoot, - blocks: newBlocks, - preservedCids: currCids.toList(), - } - } - - async processRebase(did: string, rebaseData: RebaseData) { - this.db.assertTransaction() - - const storage = new SqlRepoStorage(this.db, did) - - const recordCountBefore = await this.countRecordBlocks(did) - await Promise.all([ - storage.applyRebase(rebaseData), - this.blobs.processRebaseBlobs(did, rebaseData.commit), - ]) - const recordCountAfter = await this.countRecordBlocks(did) - // This is purely a dummy check on a very sensitive operation - if (recordCountBefore !== recordCountAfter) { - throw new Error( - `Record blocks deleted during rebase. Rolling back: ${did}`, - ) - } - - await this.afterRebaseProcessing(did, rebaseData) - } - - async afterRebaseProcessing(did: string, rebaseData: RebaseData) { - const seqEvt = await sequencer.formatSeqRebase(did, rebaseData) - await sequencer.sequenceEvt(this.db, seqEvt) - } - - // used for integrity check - private async countRecordBlocks(did: string): Promise { - const res = await this.db.db - .selectFrom('record') - .where('record.did', '=', did) - .innerJoin('ipld_block', (join) => - join - .onRef('ipld_block.creator', '=', 'record.did') - .onRef('ipld_block.cid', '=', 'record.cid'), - ) - .select(countAll.as('count')) - .executeTakeFirst() - return res?.count ?? 0 - } - async deleteRepo(did: string) { // Not done in transaction because it would be too long, prone to contention. // Also, this can safely be run multiple times if it fails. // delete all blocks from this did & no other did await this.db.db.deleteFrom('repo_root').where('did', '=', did).execute() await this.db.db.deleteFrom('repo_seq').where('did', '=', did).execute() - await this.db.db - .deleteFrom('repo_commit_block') - .where('creator', '=', did) - .execute() - await this.db.db - .deleteFrom('repo_commit_history') - .where('creator', '=', did) - .execute() await this.db.db .deleteFrom('ipld_block') .where('creator', '=', did) diff --git a/packages/pds/src/sql-repo-storage.ts b/packages/pds/src/sql-repo-storage.ts index 0b9928c0371..af3cf719822 100644 --- a/packages/pds/src/sql-repo-storage.ts +++ b/packages/pds/src/sql-repo-storage.ts @@ -3,19 +3,17 @@ import { RepoStorage, BlockMap, CidSet, - RebaseData, - CommitCidData, + ReadableBlockstore, + writeCarStream, } from '@atproto/repo' import { chunkArray } from '@atproto/common' import { CID } from 'multiformats/cid' import Database from './db' -import { valuesList } from './db/util' import { IpldBlock } from './db/tables/ipld-block' -import { RepoCommitBlock } from './db/tables/repo-commit-block' -import { RepoCommitHistory } from './db/tables/repo-commit-history' import { ConcurrentWriteError } from './services/repo' +import { sql } from 'kysely' -export class SqlRepoStorage extends RepoStorage { +export class SqlRepoStorage extends ReadableBlockstore implements RepoStorage { cache: BlockMap = new BlockMap() constructor( @@ -32,7 +30,7 @@ export class SqlRepoStorage extends RepoStorage { return this.db.txAdvisoryLock(this.did) } - async getHead(): Promise { + async getRoot(): Promise { const res = await this.db.db .selectFrom('repo_root') .selectAll() @@ -42,17 +40,25 @@ export class SqlRepoStorage extends RepoStorage { return CID.parse(res.root) } + async getRootDetailed(): Promise<{ cid: CID; rev: string } | null> { + const res = await this.db.db + .selectFrom('repo_root') + .selectAll() + .where('did', '=', this.did) + .executeTakeFirst() + if (!res) return null + return { + cid: CID.parse(res.root), + rev: res.rev ?? '', // @TODO add not-null constraint to rev + } + } + // proactively cache all blocks from a particular commit (to prevent multiple roundtrips) - async cacheCommit(cid: CID): Promise { + async cacheRev(rev: string): Promise { const res = await this.db.db - .selectFrom('repo_commit_block') - .innerJoin('ipld_block', (join) => - join - .onRef('ipld_block.cid', '=', 'repo_commit_block.block') - .onRef('ipld_block.creator', '=', 'repo_commit_block.creator'), - ) - .where('repo_commit_block.creator', '=', this.did) - .where('repo_commit_block.commit', '=', cid.toString()) + .selectFrom('ipld_block') + .where('creator', '=', this.did) + .where('repoRev', '=', rev) .select(['ipld_block.cid', 'ipld_block.content']) .execute() for (const row of res) { @@ -105,13 +111,14 @@ export class SqlRepoStorage extends RepoStorage { return { blocks, missing: missing.toList() } } - async putBlock(cid: CID, block: Uint8Array): Promise { + async putBlock(cid: CID, block: Uint8Array, rev: string): Promise { this.db.assertTransaction() await this.db.db .insertInto('ipld_block') .values({ cid: cid.toString(), creator: this.did, + repoRev: rev, size: block.length, content: block, }) @@ -120,13 +127,14 @@ export class SqlRepoStorage extends RepoStorage { this.cache.set(cid, block) } - async putMany(toPut: BlockMap): Promise { + async putMany(toPut: BlockMap, rev: string): Promise { this.db.assertTransaction() const blocks: IpldBlock[] = [] toPut.forEach((bytes, cid) => { blocks.push({ cid: cid.toString(), creator: this.did, + repoRev: rev, size: bytes.length, content: bytes, }) @@ -143,108 +151,26 @@ export class SqlRepoStorage extends RepoStorage { ) } - async applyRebase(rebase: RebaseData): Promise { - this.db.assertTransaction() - await Promise.all([ - this.db.db - .deleteFrom('repo_commit_block') - .where('creator', '=', this.did) - .execute(), - this.db.db - .deleteFrom('repo_commit_history') - .where('creator', '=', this.did) - .execute(), - this.putMany(rebase.blocks), - ]) - - const allCids = [...rebase.preservedCids, ...rebase.blocks.cids()] - await this.indexCommitCids([ - { commit: rebase.commit, prev: null, cids: allCids }, - ]) + async deleteMany(cids: CID[]) { + if (cids.length < 1) return + const cidStrs = cids.map((c) => c.toString()) await this.db.db .deleteFrom('ipld_block') - .where('ipld_block.creator', '=', this.did) - .whereNotExists((qb) => - qb - .selectFrom('repo_commit_block') - .selectAll() - .where('repo_commit_block.creator', '=', this.did) - .where('repo_commit_block.commit', '=', rebase.commit.toString()) - .whereRef('repo_commit_block.block', '=', 'ipld_block.cid'), - ) + .where('creator', '=', this.did) + .where('cid', 'in', cidStrs) .execute() - await this.updateHead(rebase.commit, rebase.rebased) - } - - async indexCommits(commits: CommitData[]): Promise { - this.db.assertTransaction() - const allBlocks = new BlockMap() - const cidData: CommitCidData[] = [] - for (const commit of commits) { - const commitCids: CID[] = [] - for (const block of commit.blocks.entries()) { - commitCids.push(block.cid) - allBlocks.set(block.cid, block.bytes) - } - cidData.push({ - commit: commit.commit, - prev: commit.prev, - cids: commitCids, - }) - } - await Promise.all([this.putMany(allBlocks), this.indexCommitCids(cidData)]) } - async indexCommitCids(commits: CommitCidData[]): Promise { - this.db.assertTransaction() - const commitBlocks: RepoCommitBlock[] = [] - const commitHistory: RepoCommitHistory[] = [] - for (const commit of commits) { - for (const cid of commit.cids) { - commitBlocks.push({ - commit: commit.commit.toString(), - block: cid.toString(), - creator: this.did, - }) - } - commitHistory.push({ - commit: commit.commit.toString(), - prev: commit.prev ? commit.prev.toString() : null, - creator: this.did, - }) - } - const insertCommitBlocks = Promise.all( - chunkArray(commitBlocks, 500).map((batch) => - this.db.db - .insertInto('repo_commit_block') - .values(batch) - .onConflict((oc) => oc.doNothing()) - .execute(), - ), - ) - const insertCommitHistory = Promise.all( - chunkArray(commitHistory, 500).map((batch) => - this.db.db - .insertInto('repo_commit_history') - .values(batch) - .onConflict((oc) => oc.doNothing()) - .execute(), - ), - ) - await Promise.all([insertCommitBlocks, insertCommitHistory]) + async applyCommit(commit: CommitData) { + await Promise.all([ + this.updateRoot(commit.cid, commit.prev ?? undefined), + this.putMany(commit.newBlocks, commit.rev), + this.deleteMany(commit.removedCids.toList()), + ]) } - async updateHead(cid: CID, prev: CID | null): Promise { - if (prev === null) { - await this.db.db - .insertInto('repo_root') - .values({ - did: this.did, - root: cid.toString(), - indexedAt: this.getTimestamp(), - }) - .execute() - } else { + async updateRoot(cid: CID, ensureSwap?: CID): Promise { + if (ensureSwap) { const res = await this.db.db .updateTable('repo_root') .set({ @@ -252,85 +178,81 @@ export class SqlRepoStorage extends RepoStorage { indexedAt: this.getTimestamp(), }) .where('did', '=', this.did) - .where('root', '=', prev.toString()) + .where('root', '=', ensureSwap.toString()) .executeTakeFirst() if (res.numUpdatedRows < 1) { throw new ConcurrentWriteError() } + } else { + await this.db.db + .insertInto('repo_root') + .values({ + did: this.did, + root: cid.toString(), + indexedAt: this.getTimestamp(), + }) + .onConflict((oc) => + oc.column('did').doUpdateSet({ + root: cid.toString(), + indexedAt: this.getTimestamp(), + }), + ) + .execute() } } - private getTimestamp(): string { - return this.timestamp || new Date().toISOString() - } - - async getCommitPath( - latest: CID, - earliest: CID | null, - ): Promise { - const res = await this.db.db - .withRecursive('ancestor(commit, prev)', (cte) => - cte - .selectFrom('repo_commit_history as commit') - .select(['commit.commit as commit', 'commit.prev as prev']) - .where('commit', '=', latest.toString()) - .where('creator', '=', this.did) - .unionAll( - cte - .selectFrom('repo_commit_history as commit') - .select(['commit.commit as commit', 'commit.prev as prev']) - .innerJoin('ancestor', (join) => - join - .onRef('ancestor.prev', '=', 'commit.commit') - .on('commit.creator', '=', this.did), - ) - .if(earliest !== null, (qb) => - // @ts-ignore - qb.where('commit.commit', '!=', earliest?.toString() as string), - ), - ), - ) - .selectFrom('ancestor') - .select('commit') - .execute() - return res.map((row) => CID.parse(row.commit)).reverse() + async getCarStream(since?: string) { + const root = await this.getRoot() + if (!root) { + throw new RepoRootNotFoundError() + } + return writeCarStream(root, async (car) => { + let cursor: RevCursor | undefined = undefined + do { + const res = await this.getBlockRange(since, cursor) + for (const row of res) { + await car.put({ + cid: CID.parse(row.cid), + bytes: row.content, + }) + } + const lastRow = res.at(-1) + if (lastRow && lastRow.repoRev) { + cursor = { + cid: CID.parse(lastRow.cid), + rev: lastRow.repoRev, + } + } else { + cursor = undefined + } + } while (cursor) + }) } - async getAllBlocksForCommits(commits: CID[]): Promise { - if (commits.length === 0) return [] - const commitStrs = commits.map((commit) => commit.toString()) - const res = await this.db.db - .selectFrom('repo_commit_block') - .where('repo_commit_block.creator', '=', this.did) - .whereRef('repo_commit_block.commit', 'in', valuesList(commitStrs)) - .innerJoin('ipld_block', (join) => - join - .onRef('ipld_block.cid', '=', 'repo_commit_block.block') - .onRef('ipld_block.creator', '=', 'repo_commit_block.creator'), + async getBlockRange(since?: string, cursor?: RevCursor) { + const { ref } = this.db.db.dynamic + let builder = this.db.db + .selectFrom('ipld_block') + .where('creator', '=', this.did) + .select(['cid', 'repoRev', 'content']) + .orderBy('repoRev', 'asc') + .orderBy('cid', 'asc') + .limit(500) + if (cursor) { + // use this syntax to ensure we hit the index + builder = builder.where( + sql`((${ref('repoRev')}, ${ref('cid')}) > (${ + cursor.rev + }, ${cursor.cid.toString()}))`, ) - .select([ - 'repo_commit_block.commit', - 'ipld_block.cid', - 'ipld_block.content', - ]) - .execute() - return res.map((row) => ({ - cid: CID.parse(row.cid), - bytes: row.content, - commit: row.commit, - })) + } else if (since) { + builder = builder.where('repoRev', '>', since) + } + return builder.execute() } - async getBlocksForCommits( - commits: CID[], - ): Promise<{ [commit: string]: BlockMap }> { - const allBlocks = await this.getAllBlocksForCommits(commits) - return allBlocks.reduce((acc, cur) => { - acc[cur.commit] ??= new BlockMap() - acc[cur.commit].set(cur.cid, cur.bytes) - this.cache.set(cur.cid, cur.bytes) - return acc - }, {}) + getTimestamp(): string { + return this.timestamp || new Date().toISOString() } async destroy(): Promise { @@ -338,10 +260,11 @@ export class SqlRepoStorage extends RepoStorage { } } -type BlockForCommit = { +type RevCursor = { cid: CID - bytes: Uint8Array - commit: string + rev: string } export default SqlRepoStorage + +export class RepoRootNotFoundError extends Error {} diff --git a/packages/pds/tests/account-deletion.test.ts b/packages/pds/tests/account-deletion.test.ts index 6720b52aee4..31ffcd1af5b 100644 --- a/packages/pds/tests/account-deletion.test.ts +++ b/packages/pds/tests/account-deletion.test.ts @@ -22,8 +22,6 @@ import { PostEmbedExternal, PostEmbedRecord, } from '../src/app-view/db/tables/post-embed' -import { RepoCommitHistory } from '../src/db/tables/repo-commit-history' -import { RepoCommitBlock } from '../src/db/tables/repo-commit-block' import { Record } from '../src/db/tables/record' import { RepoSeq } from '../src/db/tables/repo-seq' import { ACKNOWLEDGE } from '../src/lexicon/types/com/atproto/admin/defs' @@ -181,14 +179,6 @@ describe('account deletion', () => { (row) => row.did === carol.did && row.eventType === 'tombstone', ).length, ).toEqual(1) - expect(updatedDbContents.commitBlocks).toEqual( - initialDbContents.commitBlocks.filter((row) => row.creator !== carol.did), - ) - expect(updatedDbContents.commitHistories).toEqual( - initialDbContents.commitHistories.filter( - (row) => row.creator !== carol.did, - ), - ) }) it('no longer stores indexed records from the user', async () => { @@ -301,8 +291,6 @@ type DbContents = { userState: UserState[] blocks: IpldBlock[] seqs: Selectable[] - commitHistories: RepoCommitHistory[] - commitBlocks: RepoCommitBlock[] records: Record[] posts: Post[] postImages: PostEmbedImage[] @@ -325,8 +313,6 @@ const getDbContents = async (db: Database): Promise => { userState, blocks, seqs, - commitHistories, - commitBlocks, records, posts, postImages, @@ -351,19 +337,6 @@ const getDbContents = async (db: Database): Promise => { .selectAll() .execute(), db.db.selectFrom('repo_seq').orderBy('id').selectAll().execute(), - db.db - .selectFrom('repo_commit_history') - .orderBy('creator') - .orderBy('commit') - .selectAll() - .execute(), - db.db - .selectFrom('repo_commit_block') - .orderBy('creator') - .orderBy('commit') - .orderBy('block') - .selectAll() - .execute(), db.db.selectFrom('record').orderBy('uri').selectAll().execute(), db.db.selectFrom('post').orderBy('uri').selectAll().execute(), db.db @@ -402,8 +375,6 @@ const getDbContents = async (db: Database): Promise => { userState, blocks, seqs, - commitHistories, - commitBlocks, records, posts, postImages, diff --git a/packages/pds/tests/crud.test.ts b/packages/pds/tests/crud.test.ts index 12128bef898..c86f750cfac 100644 --- a/packages/pds/tests/crud.test.ts +++ b/packages/pds/tests/crud.test.ts @@ -377,6 +377,35 @@ describe('crud operations', () => { }) await expect(attemptDelete).resolves.toBeDefined() }) + + it('does not delete the underlying block if it is referenced elsewhere', async () => { + const { repo } = aliceAgent.api.com.atproto + const record = { text: 'post', createdAt: new Date().toISOString() } + const { data: post1 } = await repo.createRecord({ + repo: alice.did, + collection: ids.AppBskyFeedPost, + record, + }) + const { data: post2 } = await repo.createRecord({ + repo: alice.did, + collection: ids.AppBskyFeedPost, + record, + }) + const uri1 = new AtUri(post1.uri) + await repo.deleteRecord({ + repo: uri1.host, + collection: uri1.collection, + rkey: uri1.rkey, + }) + const uri2 = new AtUri(post2.uri) + const checkPost2 = await repo.getRecord({ + repo: uri2.host, + collection: uri2.collection, + rkey: uri2.rkey, + }) + expect(checkPost2).toBeDefined() + expect(checkPost2.data.value).toMatchObject(record) + }) }) describe('putRecord', () => { @@ -565,11 +594,11 @@ describe('crud operations', () => { it('createRecord succeeds on proper commit cas', async () => { const { repo, sync } = aliceAgent.api.com.atproto - const { data: head } = await sync.getHead({ did: alice.did }) + const { data: commit } = await sync.getLatestCommit({ did: alice.did }) const { data: post } = await repo.createRecord({ repo: alice.did, collection: ids.AppBskyFeedPost, - swapCommit: head.root, + swapCommit: commit.cid, record: postRecord(), }) const uri = new AtUri(post.uri) @@ -583,7 +612,9 @@ describe('crud operations', () => { it('createRecord fails on bad commit cas', async () => { const { repo, sync } = aliceAgent.api.com.atproto - const { data: staleHead } = await sync.getHead({ did: alice.did }) + const { data: staleCommit } = await sync.getLatestCommit({ + did: alice.did, + }) // Update repo, change head await repo.createRecord({ repo: alice.did, @@ -593,7 +624,7 @@ describe('crud operations', () => { const attemptCreate = repo.createRecord({ repo: alice.did, collection: ids.AppBskyFeedPost, - swapCommit: staleHead.root, + swapCommit: staleCommit.cid, record: postRecord(), }) await expect(attemptCreate).rejects.toThrow(createRecord.InvalidSwapError) @@ -606,13 +637,13 @@ describe('crud operations', () => { collection: ids.AppBskyFeedPost, record: postRecord(), }) - const { data: head } = await sync.getHead({ did: alice.did }) + const { data: commit } = await sync.getLatestCommit({ did: alice.did }) const uri = new AtUri(post.uri) await repo.deleteRecord({ repo: uri.host, collection: uri.collection, rkey: uri.rkey, - swapCommit: head.root, + swapCommit: commit.cid, }) const checkPost = repo.getRecord({ repo: uri.host, @@ -624,7 +655,9 @@ describe('crud operations', () => { it('deleteRecord fails on bad commit cas', async () => { const { repo, sync } = aliceAgent.api.com.atproto - const { data: staleHead } = await sync.getHead({ did: alice.did }) + const { data: staleCommit } = await sync.getLatestCommit({ + did: alice.did, + }) const { data: post } = await repo.createRecord({ repo: alice.did, collection: ids.AppBskyFeedPost, @@ -635,7 +668,7 @@ describe('crud operations', () => { repo: uri.host, collection: uri.collection, rkey: uri.rkey, - swapCommit: staleHead.root, + swapCommit: staleCommit.cid, }) await expect(attemptDelete).rejects.toThrow(deleteRecord.InvalidSwapError) const checkPost = repo.getRecord({ @@ -693,12 +726,12 @@ describe('crud operations', () => { it('putRecord succeeds on proper commit cas', async () => { const { repo, sync } = aliceAgent.api.com.atproto - const { data: head } = await sync.getHead({ did: alice.did }) + const { data: commit } = await sync.getLatestCommit({ did: alice.did }) const { data: profile } = await repo.putRecord({ repo: alice.did, collection: ids.AppBskyActorProfile, rkey: 'self', - swapCommit: head.root, + swapCommit: commit.cid, record: profileRecord(), }) const { data: checkProfile } = await repo.getRecord({ @@ -711,7 +744,9 @@ describe('crud operations', () => { it('putRecord fails on bad commit cas', async () => { const { repo, sync } = aliceAgent.api.com.atproto - const { data: staleHead } = await sync.getHead({ did: alice.did }) + const { data: staleCommit } = await sync.getLatestCommit({ + did: alice.did, + }) // Update repo, change head await repo.createRecord({ repo: alice.did, @@ -722,7 +757,7 @@ describe('crud operations', () => { repo: alice.did, collection: ids.AppBskyActorProfile, rkey: 'self', - swapCommit: staleHead.root, + swapCommit: staleCommit.cid, record: profileRecord(), }) await expect(attemptPut).rejects.toThrow(putRecord.InvalidSwapError) @@ -790,10 +825,10 @@ describe('crud operations', () => { it('applyWrites succeeds on proper commit cas', async () => { const { repo, sync } = aliceAgent.api.com.atproto - const { data: head } = await sync.getHead({ did: alice.did }) + const { data: commit } = await sync.getLatestCommit({ did: alice.did }) await repo.applyWrites({ repo: alice.did, - swapCommit: head.root, + swapCommit: commit.cid, writes: [ { $type: `${ids.ComAtprotoRepoApplyWrites}#create`, @@ -807,7 +842,9 @@ describe('crud operations', () => { it('applyWrites fails on bad commit cas', async () => { const { repo, sync } = aliceAgent.api.com.atproto - const { data: staleHead } = await sync.getHead({ did: alice.did }) + const { data: staleCommit } = await sync.getLatestCommit({ + did: alice.did, + }) // Update repo, change head await repo.createRecord({ repo: alice.did, @@ -816,7 +853,7 @@ describe('crud operations', () => { }) const attemptApplyWrite = repo.applyWrites({ repo: alice.did, - swapCommit: staleHead.root, + swapCommit: staleCommit.cid, writes: [ { $type: `${ids.ComAtprotoRepoApplyWrites}#create`, diff --git a/packages/pds/tests/db.test.ts b/packages/pds/tests/db.test.ts index 1d9501f1d58..2f583b0073f 100644 --- a/packages/pds/tests/db.test.ts +++ b/packages/pds/tests/db.test.ts @@ -31,6 +31,7 @@ describe('db', () => { .values({ did: 'x', root: 'x', + rev: 'x', indexedAt: 'bad-date', }) .returning('did') @@ -52,6 +53,7 @@ describe('db', () => { expect(row).toEqual({ did: 'x', root: 'x', + rev: 'x', indexedAt: 'bad-date', takedownId: null, }) diff --git a/packages/pds/tests/migrations/repo-version-upgrade.test.ts b/packages/pds/tests/migrations/repo-version-upgrade.test.ts new file mode 100644 index 00000000000..d73bc8cc7ae --- /dev/null +++ b/packages/pds/tests/migrations/repo-version-upgrade.test.ts @@ -0,0 +1,173 @@ +import AtpAgent from '@atproto/api' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' +import { TestNetworkNoAppView } from '@atproto/dev-env' +import { randomBytes } from 'crypto' +import { TID, cidForCbor } from '@atproto/common' +import { IpldBlock } from '../../src/db/tables/ipld-block' +import { readCarWithRoot, verifyRepo } from '@atproto/repo' +import { Database } from '../../src' + +describe('repo version upgrade', () => { + let network: TestNetworkNoAppView + let db: Database + let agent: AtpAgent + let sc: SeedClient + + let alice: string + + beforeAll(async () => { + network = await TestNetworkNoAppView.create({ + dbPostgresSchema: 'repo_version_upgrade', + }) + db = network.pds.ctx.db + agent = network.pds.getClient() + sc = new SeedClient(agent) + await basicSeed(sc) + alice = sc.dids.alice + }) + + afterAll(async () => { + await network.close() + }) + + const getNonAliceData = async () => { + const ipldBlocksQb = db.db + .selectFrom('ipld_block') + .where('creator', '!=', alice) + .selectAll() + .orderBy('creator') + .orderBy('cid') + const recordsQb = db.db + .selectFrom('record') + .where('did', '!=', alice) + .selectAll() + .orderBy('did') + .orderBy('uri') + const repoBlobsQb = db.db + .selectFrom('repo_blob') + .where('did', '!=', alice) + .selectAll() + .orderBy('did') + .orderBy('cid') + const repoRootsQb = db.db + .selectFrom('repo_root') + .where('did', '!=', alice) + .selectAll() + .orderBy('did') + const [ipldBlocks, records, repoBlobs, repoRoots] = await Promise.all([ + ipldBlocksQb.execute(), + recordsQb.execute(), + repoBlobsQb.execute(), + repoRootsQb.execute(), + ]) + return { + ipldBlocks, + records, + repoBlobs, + repoRoots, + } + } + + const addCruft = async (did: string) => { + const cruft: IpldBlock[] = [] + for (let i = 0; i < 1000; i++) { + const bytes = randomBytes(128) + const cid = await cidForCbor(bytes) + cruft.push({ + cid: cid.toString(), + creator: did, + repoRev: Math.random() > 0.5 ? TID.nextStr() : null, + size: 128, + content: bytes, + }) + } + await db.db.insertInto('ipld_block').values(cruft).execute() + return cruft + } + + const fetchAndVerifyRepo = async (did: string) => { + const res = await agent.api.com.atproto.sync.getRepo({ + did, + }) + const car = await readCarWithRoot(res.data) + return verifyRepo( + car.blocks, + car.root, + alice, + network.pds.ctx.repoSigningKey.did(), + ) + } + + it('upgrades a repo', async () => { + const nonAliceDataBefore = await getNonAliceData() + const aliceRepoBefore = await fetchAndVerifyRepo(alice) + + const cruft = await addCruft(alice) + + await agent.api.com.atproto.temp.upgradeRepoVersion( + { did: alice }, + { + headers: network.pds.adminAuthHeaders('admin'), + encoding: 'application/json', + }, + ) + + const nonAliceDataAfter = await getNonAliceData() + + // does not affect other users + expect(nonAliceDataAfter).toEqual(nonAliceDataBefore) + + // cleans up cruft + const res = await db.db + .selectFrom('ipld_block') + .selectAll() + .where('creator', '=', alice) + .execute() + const cidSet = new Set(res.map((row) => row.cid)) + for (const row of cruft) { + expect(cidSet.has(row.cid)).toBe(false) + } + + const aliceRepoAfter = await fetchAndVerifyRepo(alice) + expect(aliceRepoAfter.creates).toEqual(aliceRepoBefore.creates) + + // it updated the repo rev on all blocks/records/blobs + const root = await db.db + .selectFrom('repo_root') + .where('did', '=', alice) + .selectAll() + .executeTakeFirst() + if (!root || !root.rev) { + throw new Error('did not set rev') + } + expect(root.root).toEqual(aliceRepoAfter.commit.cid.toString()) + const nonUpgradedRecords = await db.db + .selectFrom('record') + .where('did', '=', alice) + .where((qb) => + qb.where('repoRev', '!=', root.rev).orWhere('repoRev', 'is', null), + ) + .selectAll() + .execute() + expect(nonUpgradedRecords.length).toBe(0) + const nonUpgradedBlocks = await db.db + .selectFrom('ipld_block') + .where('creator', '=', alice) + .where((qb) => + qb.where('repoRev', '!=', root.rev).orWhere('repoRev', 'is', null), + ) + .selectAll() + .execute() + expect(nonUpgradedBlocks.length).toBe(0) + const nonUpgradedBlobs = await db.db + .selectFrom('repo_blob') + .where('did', '=', alice) + .where((qb) => + qb.where('repoRev', '!=', root.rev).orWhere('repoRev', 'is', null), + ) + .selectAll() + .execute() + expect(nonUpgradedBlobs.length).toBe(0) + }) +}) diff --git a/packages/pds/tests/races.test.ts b/packages/pds/tests/races.test.ts index dad41a0d8ae..7f276e61147 100644 --- a/packages/pds/tests/races.test.ts +++ b/packages/pds/tests/races.test.ts @@ -4,7 +4,7 @@ import AppContext from '../src/context' import { PreparedWrite, prepareCreate } from '../src/repo' import { wait } from '@atproto/common' import SqlRepoStorage from '../src/sql-repo-storage' -import { CommitData, MemoryBlockstore, loadFullRepo } from '@atproto/repo' +import { CommitData, readCarWithRoot, verifyRepo } from '@atproto/repo' import { ConcurrentWriteError } from '../src/services/repo' describe('crud operations', () => { @@ -89,19 +89,17 @@ describe('crud operations', () => { const listed = await agent.api.app.bsky.feed.post.list({ repo: did }) expect(listed.records.length).toBe(2) - const repoCar = await agent.api.com.atproto.sync.getRepo({ did }) - const storage = new MemoryBlockstore() - const verified = await loadFullRepo( - storage, - repoCar.data, + const carRes = await agent.api.com.atproto.sync.getRepo({ did }) + const car = await readCarWithRoot(carRes.data) + const verified = await verifyRepo( + car.blocks, + car.root, did, ctx.repoSigningKey.did(), ) - // it split writes over 2 commits - expect(verified.writeLog[1].length).toBe(1) - expect(verified.writeLog[2].length).toBe(1) - expect(verified.writeLog[1][0].cid.equals(write.cid)).toBeTruthy() - expect(verified.writeLog[2][0].cid.toString()).toEqual( + expect(verified.creates.length).toBe(2) + expect(verified.creates[0].cid.equals(write.cid)).toBeTruthy() + expect(verified.creates[1].cid.toString()).toEqual( createdPost.cid.toString(), ) }) diff --git a/packages/pds/tests/rebase.test.ts b/packages/pds/tests/rebase.test.ts deleted file mode 100644 index 7d771064cf2..00000000000 --- a/packages/pds/tests/rebase.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import AtpAgent from '@atproto/api' -import * as repo from '@atproto/repo' -import { MemoryBlockstore } from '@atproto/repo' -import { AppContext } from '../src' -import { CloseFn, runTestServer } from './_util' -import { SeedClient } from './seeds/client' - -describe('repo rebases', () => { - let agent: AtpAgent - let sc: SeedClient - let alice: string - - let ctx: AppContext - - let close: CloseFn - - beforeAll(async () => { - const server = await runTestServer({ - dbPostgresSchema: 'repo_rebase', - }) - ctx = server.ctx - close = server.close - agent = new AtpAgent({ service: server.url }) - sc = new SeedClient(agent) - await sc.createAccount('alice', { - email: 'alice@test.com', - handle: 'alice.test', - password: 'alice-pass', - }) - alice = sc.dids.alice - }) - - afterAll(async () => { - await close() - }) - - it('handles rebases', async () => { - for (let i = 0; i < 40; i++) { - await sc.post(alice, `post-${i}`) - } - - const carBefore = await agent.api.com.atproto.sync.getRepo({ did: alice }) - - await agent.api.com.atproto.repo.rebaseRepo( - { repo: alice }, - { encoding: 'application/json', headers: sc.getHeaders(alice) }, - ) - - const commitPath = await agent.api.com.atproto.sync.getCommitPath({ - did: alice, - }) - expect(commitPath.data.commits.length).toBe(1) - const carAfter = await agent.api.com.atproto.sync.getRepo({ did: alice }) - - const before = await repo.loadFullRepo( - new MemoryBlockstore(), - carBefore.data, - alice, - ctx.repoSigningKey.did(), - ) - const after = await repo.loadFullRepo( - new MemoryBlockstore(), - carAfter.data, - alice, - ctx.repoSigningKey.did(), - ) - const contentsBefore = await before.repo.getContents() - const contentsAfter = await after.repo.getContents() - expect(contentsAfter).toEqual(contentsBefore) - expect(after.repo.commit.prev).toBe(null) - - const repoBlobs = await ctx.db.db - .selectFrom('repo_blob') - .where('did', '=', alice) - .selectAll() - .execute() - expect( - repoBlobs.every((row) => row.commit === commitPath[0].tostring()), - ).toBeTruthy() - }) -}) diff --git a/packages/pds/tests/sql-repo-storage.test.ts b/packages/pds/tests/sql-repo-storage.test.ts index 6f5069c615f..c19a8b41805 100644 --- a/packages/pds/tests/sql-repo-storage.test.ts +++ b/packages/pds/tests/sql-repo-storage.test.ts @@ -1,9 +1,10 @@ -import { range, dataToCborBlock } from '@atproto/common' -import { def } from '@atproto/repo' +import { range, dataToCborBlock, TID } from '@atproto/common' +import { CidSet, def } from '@atproto/repo' import BlockMap from '@atproto/repo/src/block-map' import { Database } from '../src' import SqlRepoStorage from '../src/sql-repo-storage' import { CloseFn, runTestServer } from './_util' +import { CID } from 'multiformats/cid' describe('sql repo storage', () => { let db: Database @@ -75,41 +76,48 @@ describe('sql repo storage', () => { blocks.slice(5, 10).forEach((block) => { blocks1.set(block.cid, block.bytes) }) + const toRemoveList = blocks0 + .entries() + .slice(0, 2) + .map((b) => b.cid) + const toRemove = new CidSet(toRemoveList) await db.transaction(async (dbTxn) => { const storage = new SqlRepoStorage(dbTxn, did) await storage.applyCommit({ - commit: commits[0].cid, + cid: commits[0].cid, + rev: TID.nextStr(), prev: null, - blocks: blocks0, + newBlocks: blocks0, + removedCids: new CidSet(), }) await storage.applyCommit({ - commit: commits[1].cid, + cid: commits[1].cid, prev: commits[0].cid, - blocks: blocks1, + rev: TID.nextStr(), + newBlocks: blocks1, + removedCids: toRemove, }) }) const storage = new SqlRepoStorage(db, did) - const head = await storage.getHead() + const head = await storage.getRoot() if (!head) { throw new Error('could not get repo head') } expect(head.toString()).toEqual(commits[1].cid.toString()) - const commitPath = await storage.getCommitPath(head, null) - if (!commitPath) { - throw new Error('could not get commit path') + + const cidsRes = await db.db + .selectFrom('ipld_block') + .where('creator', '=', did) + .select('cid') + .execute() + const allCids = new CidSet(cidsRes.map((row) => CID.parse(row.cid))) + for (const entry of blocks1.entries()) { + expect(allCids.has(entry.cid)).toBe(true) } - expect(commitPath.length).toBe(2) - expect(commitPath[0].equals(commits[0].cid)).toBeTruthy() - expect(commitPath[1].equals(commits[1].cid)).toBeTruthy() - const commitData = await storage.getCommits(head, null) - if (!commitData) { - throw new Error('could not get commit data') + for (const entry of blocks0.entries()) { + const shouldHave = !toRemove.has(entry.cid) + expect(allCids.has(entry.cid)).toBe(shouldHave) } - expect(commitData.length).toBe(2) - expect(commitData[0].commit.equals(commits[0].cid)).toBeTruthy() - expect(commitData[0].blocks.equals(blocks0)).toBeTruthy() - expect(commitData[1].commit.equals(commits[1].cid)).toBeTruthy() - expect(commitData[1].blocks.equals(blocks1)).toBeTruthy() }) }) diff --git a/packages/pds/tests/sync/subscribe-repos.test.ts b/packages/pds/tests/sync/subscribe-repos.test.ts index ad5ab1c14aa..9c8c98459ce 100644 --- a/packages/pds/tests/sync/subscribe-repos.test.ts +++ b/packages/pds/tests/sync/subscribe-repos.test.ts @@ -8,12 +8,7 @@ import { } from '@atproto/common' import { randomStr } from '@atproto/crypto' import * as repo from '@atproto/repo' -import { - getWriteLog, - MemoryBlockstore, - readCar, - WriteOpAction, -} from '@atproto/repo' +import { readCar } from '@atproto/repo' import { byFrame, ErrorFrame, Frame, MessageFrame } from '@atproto/xrpc-server' import { WebSocket } from 'ws' import { @@ -63,16 +58,10 @@ describe('repo subscribe repos', () => { await close() }) - const getRepo = async (did: string) => { - const car = await agent.api.com.atproto.sync.getRepo({ did }) - const storage = new MemoryBlockstore() - const synced = await repo.loadFullRepo( - storage, - new Uint8Array(car.data), - did, - ctx.repoSigningKey.did(), - ) - return repo.Repo.load(storage, synced.root) + const getRepo = async (did: string): Promise => { + const carRes = await agent.api.com.atproto.sync.getRepo({ did }) + const car = await repo.readCarWithRoot(carRes.data) + return repo.verifyRepo(car.blocks, car.root, did, ctx.repoSigningKey.did()) } const getHandleEvts = (frames: Frame[]): HandleEvt[] => { @@ -148,33 +137,46 @@ describe('repo subscribe repos', () => { } const verifyRepo = async (did: string, evts: CommitEvt[]) => { - const didRepo = await getRepo(did) - const writeLog = await getWriteLog(didRepo.storage, didRepo.cid, null) - const commits = await didRepo.storage.getCommits(didRepo.cid, null) - if (!commits) { - return expect(commits !== null) + const fromRpc = await getRepo(did) + const contents = {} as Record> + const allBlocks = new repo.BlockMap() + for (const evt of evts) { + const car = await readCar(evt.blocks) + allBlocks.addMap(car.blocks) + for (const op of evt.ops) { + const { collection, rkey } = repo.parseDataKey(op.path) + if (op.action === 'delete') { + delete contents[collection][rkey] + } else { + if (op.cid) { + contents[collection] ??= {} + contents[collection][rkey] ??= op.cid + } + } + } } - expect(evts.length).toBe(commits.length) - expect(evts.length).toBe(writeLog.length) - for (let i = 0; i < commits.length; i++) { - const commit = commits[i] - const evt = evts[i] - expect(evt.repo).toEqual(did) - expect(evt.commit.toString()).toEqual(commit.commit.toString()) - expect(evt.prev?.toString()).toEqual(commits[i - 1]?.commit?.toString()) - const car = await repo.readCarWithRoot(evt.blocks as Uint8Array) - expect(car.root.equals(commit.commit)) - expect(car.blocks.equals(commit.blocks)) - const writes = writeLog[i].map((w) => ({ - action: w.action, - path: w.collection + '/' + w.rkey, - cid: w.action === WriteOpAction.Delete ? null : w.cid.toString(), - })) - const sortedOps = evt.ops - .sort((a, b) => a.path.localeCompare(b.path)) - .map((op) => ({ ...op, cid: op.cid?.toString() ?? null })) - const sortedWrites = writes.sort((a, b) => a.path.localeCompare(b.path)) - expect(sortedOps).toEqual(sortedWrites) + for (const write of fromRpc.creates) { + expect(contents[write.collection][write.rkey].equals(write.cid)).toBe( + true, + ) + } + const lastCommit = evts.at(-1)?.commit + if (!lastCommit) { + throw new Error('no last commit') + } + const fromStream = await repo.verifyRepo( + allBlocks, + lastCommit, + did, + ctx.repoSigningKey.did(), + ) + const fromRpcOps = fromRpc.creates + const fromStreamOps = fromStream.creates + expect(fromStreamOps.length).toEqual(fromRpcOps.length) + for (let i = 0; i < fromRpcOps.length; i++) { + expect(fromStreamOps[i].collection).toEqual(fromRpcOps[i].collection) + expect(fromStreamOps[i].rkey).toEqual(fromRpcOps[i].rkey) + expect(fromStreamOps[i].cid).toEqual(fromRpcOps[i].cid) } } @@ -197,7 +199,7 @@ describe('repo subscribe repos', () => { const isDone = async (evt: any) => { if (evt === undefined) return false if (evt instanceof ErrorFrame) return true - const caughtUp = await ctx.sequencerLeader.isCaughtUp() + const caughtUp = await ctx.sequencerLeader?.isCaughtUp() if (!caughtUp) return false const curr = await db.db .selectFrom('repo_seq') @@ -415,43 +417,6 @@ describe('repo subscribe repos', () => { verifyHandleEvent(handleEvts[1], bob, 'bob5.test') }) - it('sync rebases', async () => { - const prevHead = await agent.api.com.atproto.sync.getHead({ did: alice }) - - await agent.api.com.atproto.repo.rebaseRepo( - { repo: alice }, - { encoding: 'application/json', headers: sc.getHeaders(alice) }, - ) - - const currHead = await agent.api.com.atproto.sync.getHead({ did: alice }) - - const ws = new WebSocket( - `ws://${serverHost}/xrpc/com.atproto.sync.subscribeRepos?cursor=${-1}`, - ) - - const gen = byFrame(ws) - const frames = await readTillCaughtUp(gen) - ws.terminate() - - const aliceEvts = getCommitEvents(alice, frames) - expect(aliceEvts.length).toBe(1) - const evt = aliceEvts[0] - expect(evt.rebase).toBe(true) - expect(evt.tooBig).toBe(false) - expect(evt.commit.toString()).toEqual(currHead.data.root) - expect(evt.prev?.toString()).toEqual(prevHead.data.root) - expect(evt.ops).toEqual([]) - expect(evt.blobs).toEqual([]) - const car = await readCar(evt.blocks) - expect(car.blocks.size).toBe(1) - expect(car.roots.length).toBe(1) - expect(car.roots[0].toString()).toEqual(currHead.data.root) - - // did not affect other users - const bobEvts = getCommitEvents(bob, frames) - expect(bobEvts.length).toBeGreaterThan(10) - }) - it('sends info frame on out of date cursor', async () => { // we rewrite the sequenceAt time for existing seqs to be past the backfill cutoff // then we create some new posts diff --git a/packages/pds/tests/sync/sync.test.ts b/packages/pds/tests/sync/sync.test.ts index 9605d285a4c..73947eddbc3 100644 --- a/packages/pds/tests/sync/sync.test.ts +++ b/packages/pds/tests/sync/sync.test.ts @@ -1,9 +1,8 @@ -import assert from 'assert' import AtpAgent from '@atproto/api' -import { cidForCbor, TID } from '@atproto/common' +import { TID } from '@atproto/common' import { randomStr } from '@atproto/crypto' import * as repo from '@atproto/repo' -import { collapseWriteLog, MemoryBlockstore, readCar } from '@atproto/repo' +import { MemoryBlockstore } from '@atproto/repo' import { AtUri } from '@atproto/syntax' import { TAKEDOWN } from '@atproto/api/src/client/types/com/atproto/admin/defs' import { CID } from 'multiformats/cid' @@ -56,21 +55,21 @@ describe('repo sync', () => { uris.push(uri) } - const car = await agent.api.com.atproto.sync.getRepo({ did }) - const synced = await repo.loadFullRepo( - storage, - new Uint8Array(car.data), + const carRes = await agent.api.com.atproto.sync.getRepo({ did }) + const car = await repo.readCarWithRoot(carRes.data) + const synced = await repo.verifyRepo( + car.blocks, + car.root, did, ctx.repoSigningKey.did(), ) - expect(synced.writeLog.length).toBe(ADD_COUNT + 1) // +1 because of repo - const ops = await collapseWriteLog(synced.writeLog) - expect(ops.length).toBe(ADD_COUNT) // Does not include empty initial commit - const loaded = await repo.Repo.load(storage, synced.root) + await storage.applyCommit(synced.commit) + expect(synced.creates.length).toBe(ADD_COUNT) + const loaded = await repo.Repo.load(storage, car.root) const contents = await loaded.getContents() expect(contents).toEqual(repoData) - currRoot = synced.root + currRoot = car.root }) it('syncs creates and deletes', async () => { @@ -95,102 +94,28 @@ describe('repo sync', () => { delete repoData[uri.collection][uri.rkey] } - const car = await agent.api.com.atproto.sync.getRepo({ - did, - earliest: currRoot?.toString(), - }) + const carRes = await agent.api.com.atproto.sync.getRepo({ did }) + const car = await repo.readCarWithRoot(carRes.data) const currRepo = await repo.Repo.load(storage, currRoot) - const synced = await repo.loadDiff( + const synced = await repo.verifyDiff( currRepo, - new Uint8Array(car.data), + car.blocks, + car.root, did, ctx.repoSigningKey.did(), ) - expect(synced.writeLog.length).toBe(ADD_COUNT + DEL_COUNT) - const ops = await collapseWriteLog(synced.writeLog) - expect(ops.length).toBe(ADD_COUNT) // -2 because of dels of new records, +2 because of dels of old records - const loaded = await repo.Repo.load(storage, synced.root) + expect(synced.writes.length).toBe(ADD_COUNT) // -2 because of dels of new records, +2 because of dels of old records + await storage.applyCommit(synced.commit) + const loaded = await repo.Repo.load(storage, car.root) const contents = await loaded.getContents() expect(contents).toEqual(repoData) - currRoot = synced.root + currRoot = car.root }) - it('syncs current root', async () => { - const root = await agent.api.com.atproto.sync.getHead({ did }) - expect(root.data.root).toEqual(currRoot?.toString()) - }) - - it('syncs commit path', async () => { - const local = await storage.getCommitPath(currRoot as CID, null) - if (!local) { - throw new Error('Could not get local commit path') - } - const localStr = local.map((c) => c.toString()) - const commitPath = await agent.api.com.atproto.sync.getCommitPath({ did }) - expect(commitPath.data.commits).toEqual(localStr) - - const partialCommitPath = await agent.api.com.atproto.sync.getCommitPath({ - did, - earliest: localStr[2], - latest: localStr[15], - }) - expect(partialCommitPath.data.commits).toEqual(localStr.slice(3, 16)) - }) - - it('syncs commit range', async () => { - const local = await storage.getCommits(currRoot as CID, null) - if (!local) { - throw new Error('Could not get local commit path') - } - const memoryStore = new MemoryBlockstore() - // first we load some baseline data (needed for parsing range) - const first = await agent.api.com.atproto.sync.getRepo({ - did, - latest: local[2].commit.toString(), - }) - const firstParsed = await repo.readCar(new Uint8Array(first.data)) - memoryStore.putMany(firstParsed.blocks) - - // then we load some commit range - const second = await agent.api.com.atproto.sync.getRepo({ - did, - earliest: local[2].commit.toString(), - latest: local[15].commit.toString(), - }) - const secondParsed = await repo.readCar(new Uint8Array(second.data)) - memoryStore.putMany(secondParsed.blocks) - - // then we verify we have all the commits in the range - const commits = await memoryStore.getCommits( - local[15].commit, - local[2].commit, - ) - if (!commits) { - throw new Error('expected commits to be defined') - } - const localSlice = local.slice(2, 15) - expect(commits.length).toBe(localSlice.length) - for (let i = 0; i < commits.length; i++) { - const fromRemote = commits[i] - const fromLocal = localSlice[i] - expect(fromRemote.commit.equals(fromLocal.commit)) - expect(fromRemote.blocks.equals(fromLocal.blocks)) - } - }) - - it('sync a repo checkout', async () => { - const car = await agent.api.com.atproto.sync.getCheckout({ did }) - const checkoutStorage = new MemoryBlockstore() - const loaded = await repo.loadCheckout( - checkoutStorage, - new Uint8Array(car.data), - did, - ctx.repoSigningKey.did(), - ) - expect(loaded.contents).toEqual(repoData) - const loadedRepo = await repo.Repo.load(checkoutStorage, loaded.root) - expect(await loadedRepo.getContents()).toEqual(repoData) + it('syncs latest repo commit', async () => { + const commit = await agent.api.com.atproto.sync.getLatestCommit({ did }) + expect(commit.data.cid).toEqual(currRoot?.toString()) }) it('sync a record proof', async () => { @@ -246,74 +171,6 @@ describe('repo sync', () => { expect(result.unverified.length).toBe(0) }) - it('sync blocks', async () => { - // let's just get some cids to reference - const collection = Object.keys(repoData)[0] - const rkey = Object.keys(repoData[collection])[0] - const proofCar = await agent.api.com.atproto.sync.getRecord({ - did, - collection, - rkey, - }) - const proofBlocks = await readCar(new Uint8Array(proofCar.data)) - const cids = proofBlocks.blocks.entries().map((e) => e.cid.toString()) - const res = await agent.api.com.atproto.sync.getBlocks({ - did, - cids, - }) - const car = await readCar(new Uint8Array(res.data)) - expect(car.roots.length).toBe(0) - expect(car.blocks.equals(proofBlocks.blocks)) - }) - - it('syncs images', async () => { - const img1 = await sc.uploadFile( - did, - 'tests/image/fixtures/key-landscape-small.jpg', - 'image/jpeg', - ) - const img2 = await sc.uploadFile( - did, - 'tests/image/fixtures/key-portrait-small.jpg', - 'image/jpeg', - ) - await sc.post(did, 'blah', undefined, [img1]) - await sc.post(did, 'blah', undefined, [img1, img2]) - await sc.post(did, 'blah', undefined, [img2]) - const res = await agent.api.com.atproto.sync.getCommitPath({ did }) - const commits = res.data.commits - const blobsForFirst = await agent.api.com.atproto.sync.listBlobs({ - did, - earliest: commits.at(-4), - latest: commits.at(-3), - }) - const blobsForSecond = await agent.api.com.atproto.sync.listBlobs({ - did, - earliest: commits.at(-3), - latest: commits.at(-2), - }) - const blobsForThird = await agent.api.com.atproto.sync.listBlobs({ - did, - earliest: commits.at(-2), - latest: commits.at(-1), - }) - const blobsForRange = await agent.api.com.atproto.sync.listBlobs({ - did, - earliest: commits.at(-4), - }) - const blobsForRepo = await agent.api.com.atproto.sync.listBlobs({ - did, - }) - const cid1 = img1.image.ref.toString() - const cid2 = img2.image.ref.toString() - - expect(blobsForFirst.data.cids).toEqual([cid1]) - expect(blobsForSecond.data.cids.sort()).toEqual([cid1, cid2].sort()) - expect(blobsForThird.data.cids).toEqual([cid2]) - expect(blobsForRange.data.cids.sort()).toEqual([cid1, cid2].sort()) - expect(blobsForRepo.data.cids.sort()).toEqual([cid1, cid2].sort()) - }) - describe('repo takedown', () => { beforeAll(async () => { await sc.takeModerationAction({ @@ -344,23 +201,9 @@ describe('repo sync', () => { await expect(tryGetRepoAdmin).resolves.toBeDefined() }) - it('does not sync current root unauthed', async () => { - const tryGetHead = agent.api.com.atproto.sync.getHead({ did }) - await expect(tryGetHead).rejects.toThrow(/Could not find root for DID/) - }) - - it('does not sync commit path unauthed', async () => { - const tryGetCommitPath = agent.api.com.atproto.sync.getCommitPath({ did }) - await expect(tryGetCommitPath).rejects.toThrow( - /Could not find root for DID/, - ) - }) - - it('does not sync a repo checkout unauthed', async () => { - const tryGetCheckout = agent.api.com.atproto.sync.getCheckout({ did }) - await expect(tryGetCheckout).rejects.toThrow( - /Could not find root for DID/, - ) + it('does not sync latest commit unauthed', async () => { + const tryGetLatest = agent.api.com.atproto.sync.getLatestCommit({ did }) + await expect(tryGetLatest).rejects.toThrow(/Could not find root for DID/) }) it('does not sync a record proof unauthed', async () => { @@ -373,29 +216,6 @@ describe('repo sync', () => { }) await expect(tryGetRecord).rejects.toThrow(/Could not find repo for DID/) }) - - it('does not sync blocks unauthed', async () => { - const cid = await cidForCbor({}) - const tryGetBlocks = agent.api.com.atproto.sync.getBlocks({ - did, - cids: [cid.toString()], - }) - await expect(tryGetBlocks).rejects.toThrow(/Could not find repo for DID/) - }) - - it('does not sync images unauthed', async () => { - // list blobs - const tryListBlobs = agent.api.com.atproto.sync.listBlobs({ did }) - await expect(tryListBlobs).rejects.toThrow(/Could not find root for DID/) - // get blob - const imageCid = sc.posts[did].at(-1)?.images[0].image.ref.toString() - assert(imageCid) - const tryGetBlob = agent.api.com.atproto.sync.getBlob({ - did, - cid: imageCid, - }) - await expect(tryGetBlob).rejects.toThrow(/blob not found/) - }) }) }) diff --git a/packages/repo/src/data-diff.ts b/packages/repo/src/data-diff.ts index bc88ca7efa6..bbf6fcf46b7 100644 --- a/packages/repo/src/data-diff.ts +++ b/packages/repo/src/data-diff.ts @@ -1,77 +1,83 @@ import { CID } from 'multiformats' import CidSet from './cid-set' -import { MST, mstDiff } from './mst' +import { MST, NodeEntry, mstDiff } from './mst' +import BlockMap from './block-map' export class DataDiff { adds: Record = {} updates: Record = {} deletes: Record = {} - newCids: CidSet = new CidSet() + newMstBlocks: BlockMap = new BlockMap() + newLeafCids: CidSet = new CidSet() removedCids: CidSet = new CidSet() static async of(curr: MST, prev: MST | null): Promise { return mstDiff(curr, prev) } - recordAdd(key: string, cid: CID): void { + async nodeAdd(node: NodeEntry) { + if (node.isLeaf()) { + this.leafAdd(node.key, node.value) + } else { + const data = await node.serialize() + this.treeAdd(data.cid, data.bytes) + } + } + + async nodeDelete(node: NodeEntry) { + if (node.isLeaf()) { + const key = node.key + const cid = node.value + this.deletes[key] = { key, cid } + this.removedCids.add(cid) + } else { + const cid = await node.getPointer() + this.treeDelete(cid) + } + } + + leafAdd(key: string, cid: CID) { this.adds[key] = { key, cid } - this.newCids.add(cid) + if (this.removedCids.has(cid)) { + this.removedCids.delete(cid) + } else { + this.newLeafCids.add(cid) + } } - recordUpdate(key: string, prev: CID, cid: CID): void { + leafUpdate(key: string, prev: CID, cid: CID) { + if (prev.equals(cid)) return this.updates[key] = { key, prev, cid } - this.newCids.add(cid) + this.removedCids.add(prev) + this.newLeafCids.add(cid) } - recordDelete(key: string, cid: CID): void { + leafDelete(key: string, cid: CID) { this.deletes[key] = { key, cid } + if (this.newLeafCids.has(cid)) { + this.newLeafCids.delete(cid) + } else { + this.removedCids.add(cid) + } } - recordNewCid(cid: CID): void { + treeAdd(cid: CID, bytes: Uint8Array) { if (this.removedCids.has(cid)) { this.removedCids.delete(cid) } else { - this.newCids.add(cid) + this.newMstBlocks.set(cid, bytes) } } - recordRemovedCid(cid: CID): void { - if (this.newCids.has(cid)) { - this.newCids.delete(cid) + treeDelete(cid: CID) { + if (this.newMstBlocks.has(cid)) { + this.newMstBlocks.delete(cid) } else { this.removedCids.add(cid) } } - addDiff(diff: DataDiff) { - for (const add of diff.addList()) { - if (this.deletes[add.key]) { - const del = this.deletes[add.key] - if (del.cid !== add.cid) { - this.recordUpdate(add.key, del.cid, add.cid) - } - delete this.deletes[add.key] - } else { - this.recordAdd(add.key, add.cid) - } - } - for (const update of diff.updateList()) { - this.recordUpdate(update.key, update.prev, update.cid) - delete this.adds[update.key] - delete this.deletes[update.key] - } - for (const del of diff.deleteList()) { - if (this.adds[del.key]) { - delete this.adds[del.key] - } else { - delete this.updates[del.key] - this.recordDelete(del.key, del.cid) - } - } - this.newCids.addSet(diff.newCids) - } - addList(): DataAdd[] { return Object.values(this.adds) } @@ -84,10 +90,6 @@ export class DataDiff { return Object.values(this.deletes) } - newCidList(): CID[] { - return this.newCids.toList() - } - updatedKeys(): string[] { const keys = [ ...Object.keys(this.adds), diff --git a/packages/repo/src/index.ts b/packages/repo/src/index.ts index 79a45f4720f..82ed2115ad9 100644 --- a/packages/repo/src/index.ts +++ b/packages/repo/src/index.ts @@ -5,6 +5,5 @@ export * from './mst' export * from './storage' export * from './sync' export * from './types' -export * from './verify' export * from './data-diff' export * from './util' diff --git a/packages/repo/src/mst/diff.ts b/packages/repo/src/mst/diff.ts index 1c903e8c5f0..20bd215fb33 100644 --- a/packages/repo/src/mst/diff.ts +++ b/packages/repo/src/mst/diff.ts @@ -5,11 +5,7 @@ import MstWalker from './walker' export const nullDiff = async (tree: MST): Promise => { const diff = new DataDiff() for await (const entry of tree.walk()) { - if (entry.isLeaf()) { - diff.recordAdd(entry.key, entry.value) - } else { - diff.recordNewCid(entry.pointer) - } + await diff.nodeAdd(entry) } return diff } @@ -31,21 +27,11 @@ export const mstDiff = async ( while (!leftWalker.status.done || !rightWalker.status.done) { // if one walker is finished, continue walking the other & logging all nodes if (leftWalker.status.done && !rightWalker.status.done) { - const node = rightWalker.status.curr - if (node.isLeaf()) { - diff.recordAdd(node.key, node.value) - } else { - diff.recordNewCid(node.pointer) - } + await diff.nodeAdd(rightWalker.status.curr) await rightWalker.advance() continue } else if (!leftWalker.status.done && rightWalker.status.done) { - const node = leftWalker.status.curr - if (node.isLeaf()) { - diff.recordDelete(node.key, node.value) - } else { - diff.recordRemovedCid(node.pointer) - } + await diff.nodeDelete(leftWalker.status.curr) await leftWalker.advance() continue } @@ -58,15 +44,15 @@ export const mstDiff = async ( if (left.isLeaf() && right.isLeaf()) { if (left.key === right.key) { if (!left.value.equals(right.value)) { - diff.recordUpdate(left.key, left.value, right.value) + diff.leafUpdate(left.key, left.value, right.value) } await leftWalker.advance() await rightWalker.advance() } else if (left.key < right.key) { - diff.recordDelete(left.key, left.value) + diff.leafDelete(left.key, left.value) await leftWalker.advance() } else { - diff.recordAdd(right.key, right.value) + diff.leafAdd(right.key, right.value) await rightWalker.advance() } continue @@ -78,27 +64,19 @@ export const mstDiff = async ( // if the higher walker is pointed at a leaf, then advance the lower walker to try to catch up the higher if (leftWalker.layer() > rightWalker.layer()) { if (left.isLeaf()) { - if (right.isLeaf()) { - diff.recordAdd(right.key, right.value) - } else { - diff.recordNewCid(right.pointer) - } + await diff.nodeAdd(right) await rightWalker.advance() } else { - diff.recordRemovedCid(left.pointer) + await diff.nodeDelete(left) await leftWalker.stepInto() } continue } else if (leftWalker.layer() < rightWalker.layer()) { if (right.isLeaf()) { - if (left.isLeaf()) { - diff.recordDelete(left.key, left.value) - } else { - diff.recordRemovedCid(left.pointer) - } + await diff.nodeDelete(left) await leftWalker.advance() } else { - diff.recordNewCid(right.pointer) + await diff.nodeAdd(right) await rightWalker.stepInto() } continue @@ -111,8 +89,8 @@ export const mstDiff = async ( await leftWalker.stepOver() await rightWalker.stepOver() } else { - diff.recordNewCid(right.pointer) - diff.recordRemovedCid(left.pointer) + await diff.nodeAdd(right) + await diff.nodeDelete(left) await leftWalker.stepInto() await rightWalker.stepInto() } @@ -121,11 +99,11 @@ export const mstDiff = async ( // finally, if one pointer is a tree and the other is a leaf, simply step into the tree if (left.isLeaf() && right.isTree()) { - diff.recordNewCid(right.pointer) + await diff.nodeAdd(right) await rightWalker.stepInto() continue } else if (left.isTree() && right.isLeaf()) { - diff.recordRemovedCid(left.pointer) + await diff.nodeDelete(left) await leftWalker.stepInto() continue } diff --git a/packages/repo/src/mst/mst.ts b/packages/repo/src/mst/mst.ts index e3255219b0e..80458d9dc61 100644 --- a/packages/repo/src/mst/mst.ts +++ b/packages/repo/src/mst/mst.ts @@ -2,7 +2,7 @@ import z from 'zod' import { CID } from 'multiformats' import { ReadableBlockstore } from '../storage' -import { schema as common, cidForCbor } from '@atproto/common' +import { schema as common, cidForCbor, dataToCborBlock } from '@atproto/common' import { BlockWriter } from '@ipld/car/api' import * as util from './util' import BlockMap from '../block-map' @@ -153,6 +153,13 @@ export class MST { // Instead we keep track of whether the pointer is outdated and only (recursively) calculate when needed async getPointer(): Promise { if (!this.outdatedPointer) return this.pointer + const { cid } = await this.serialize() + this.pointer = cid + this.outdatedPointer = false + return this.pointer + } + + async serialize(): Promise<{ cid: CID; bytes: Uint8Array }> { let entries = await this.getEntries() const outdated = entries.filter( (e) => e.isTree() && e.outdatedPointer, @@ -161,9 +168,12 @@ export class MST { await Promise.all(outdated.map((e) => e.getPointer())) entries = await this.getEntries() } - this.pointer = await util.cidForEntries(entries) - this.outdatedPointer = false - return this.pointer + const data = util.serializeNodeData(entries) + const block = await dataToCborBlock(data) + return { + cid: block.cid, + bytes: block.bytes, + } } // In most cases, we get the layer of a node from a hint on creation diff --git a/packages/repo/src/readable-repo.ts b/packages/repo/src/readable-repo.ts index 0ec530bb7fd..4381bd5e0e4 100644 --- a/packages/repo/src/readable-repo.ts +++ b/packages/repo/src/readable-repo.ts @@ -1,5 +1,5 @@ import { CID } from 'multiformats/cid' -import { Commit, def, RepoContents } from './types' +import { def, RepoContents, Commit } from './types' import { ReadableBlockstore } from './storage' import { MST } from './mst' import log from './logger' @@ -28,13 +28,13 @@ export class ReadableRepo { } static async load(storage: ReadableBlockstore, commitCid: CID) { - const commit = await storage.readObj(commitCid, def.commit) + const commit = await storage.readObj(commitCid, def.versionedCommit) const data = await MST.load(storage, commit.data) log.info({ did: commit.did }, 'loaded repo for') return new ReadableRepo({ storage, data, - commit, + commit: util.ensureV3Commit(commit), cid: commitCid, }) } diff --git a/packages/repo/src/repo.ts b/packages/repo/src/repo.ts index 181944ec1d9..49e8ef24810 100644 --- a/packages/repo/src/repo.ts +++ b/packages/repo/src/repo.ts @@ -3,12 +3,11 @@ import { TID } from '@atproto/common' import * as crypto from '@atproto/crypto' import { Commit, + CommitData, def, RecordCreateOp, RecordWriteOp, - CommitData, WriteOpAction, - RebaseData, } from './types' import { RepoStorage } from './storage' import { MST } from './mst' @@ -47,25 +46,29 @@ export class Repo extends ReadableRepo { const dataKey = util.formatDataKey(record.collection, record.rkey) data = await data.add(dataKey, cid) } + const dataCid = await data.getPointer() + const diff = await DataDiff.of(data, null) + newBlocks.addMap(diff.newMstBlocks) - const unstoredData = await data.getUnstoredBlocks() - newBlocks.addMap(unstoredData.blocks) - + const rev = TID.nextStr() const commit = await util.signCommit( { did, - version: 2, - prev: null, - data: unstoredData.root, + version: 3, + rev, + prev: null, // added for backwards compatibility with v2 + data: dataCid, }, keypair, ) const commitCid = await newBlocks.add(commit) - return { - commit: commitCid, + cid: commitCid, + rev, + since: null, prev: null, - blocks: newBlocks, + newBlocks, + removedCids: diff.removedCids, } } @@ -74,7 +77,7 @@ export class Repo extends ReadableRepo { commit: CommitData, ): Promise { await storage.applyCommit(commit) - return Repo.load(storage, commit.commit) + return Repo.load(storage, commit.cid) } static async create( @@ -93,17 +96,17 @@ export class Repo extends ReadableRepo { } static async load(storage: RepoStorage, cid?: CID) { - const commitCid = cid || (await storage.getHead()) + const commitCid = cid || (await storage.getRoot()) if (!commitCid) { throw new Error('No cid provided and none in storage') } - const commit = await storage.readObj(commitCid, def.commit) + const commit = await storage.readObj(commitCid, def.versionedCommit) const data = await MST.load(storage, commit.data) log.info({ did: commit.did }, 'loaded repo for') return new Repo({ storage, data, - commit, + commit: util.ensureV3Commit(commit), cid: commitCid, }) } @@ -113,16 +116,16 @@ export class Repo extends ReadableRepo { keypair: crypto.Keypair, ): Promise { const writes = Array.isArray(toWrite) ? toWrite : [toWrite] - const commitBlocks = new BlockMap() + const leaves = new BlockMap() let data = this.data for (const write of writes) { if (write.action === WriteOpAction.Create) { - const cid = await commitBlocks.add(write.record) + const cid = await leaves.add(write.record) const dataKey = write.collection + '/' + write.rkey data = await data.add(dataKey, cid) } else if (write.action === WriteOpAction.Update) { - const cid = await commitBlocks.add(write.record) + const cid = await leaves.add(write.record) const dataKey = write.collection + '/' + write.rkey data = await data.update(dataKey, cid) } else if (write.action === WriteOpAction.Delete) { @@ -131,47 +134,50 @@ export class Repo extends ReadableRepo { } } - const unstoredData = await data.getUnstoredBlocks() - commitBlocks.addMap(unstoredData.blocks) - - // ensure we're not missing any blocks that were removed and then readded in this commit + const dataCid = await data.getPointer() const diff = await DataDiff.of(data, this.data) - const found = commitBlocks.getMany(diff.newCidList()) - if (found.missing.length > 0) { - const fromStorage = await this.storage.getBlocks(found.missing) - if (fromStorage.missing.length > 0) { - // this shouldn't ever happen - throw new Error( - 'Could not find block for commit in Datastore or storage', - ) - } - commitBlocks.addMap(fromStorage.blocks) + const newBlocks = diff.newMstBlocks + const removedCids = diff.removedCids + + const addedLeaves = leaves.getMany(diff.newLeafCids.toList()) + if (addedLeaves.missing.length > 0) { + throw new Error(`Missing leaf blocks: ${addedLeaves.missing}`) } + newBlocks.addMap(addedLeaves.blocks) const rev = TID.nextStr(this.commit.rev) const commit = await util.signCommit( { did: this.did, - version: 2, - prev: this.cid, - data: unstoredData.root, + version: 3, rev, + prev: null, // added for backwards compatibility with v2 + data: dataCid, }, keypair, ) - const commitCid = await commitBlocks.add(commit) + const commitCid = await newBlocks.add(commit) + + // ensure the commit cid actually changed + if (commitCid.equals(this.cid)) { + newBlocks.delete(commitCid) + } else { + removedCids.add(this.cid) + } return { - commit: commitCid, - prev: this.cid, + cid: commitCid, rev, - blocks: commitBlocks, + since: this.commit.rev, + prev: this.cid, + newBlocks, + removedCids, } } async applyCommit(commitData: CommitData): Promise { await this.storage.applyCommit(commitData) - return Repo.load(this.storage, commitData.commit) + return Repo.load(this.storage, commitData.cid) } async applyWrites( @@ -181,37 +187,6 @@ export class Repo extends ReadableRepo { const commit = await this.formatCommit(toWrite, keypair) return this.applyCommit(commit) } - - async formatRebase(keypair: crypto.Keypair): Promise { - const preservedCids = await this.data.allCids() - const blocks = new BlockMap() - const commit = await util.signCommit( - { - did: this.did, - version: 2, - prev: null, - data: this.commit.data, - }, - keypair, - ) - const commitCid = await blocks.add(commit) - return { - commit: commitCid, - rebased: this.cid, - blocks, - preservedCids: preservedCids.toList(), - } - } - - async applyRebase(rebase: RebaseData): Promise { - await this.storage.applyRebase(rebase) - return Repo.load(this.storage, rebase.commit) - } - - async rebase(keypair: crypto.Keypair): Promise { - const rebaseData = await this.formatRebase(keypair) - return this.applyRebase(rebaseData) - } } export default Repo diff --git a/packages/repo/src/storage/index.ts b/packages/repo/src/storage/index.ts index c5eb715d59a..15d39822a51 100644 --- a/packages/repo/src/storage/index.ts +++ b/packages/repo/src/storage/index.ts @@ -1,5 +1,4 @@ export * from './readable-blockstore' -export * from './repo-storage' export * from './memory-blockstore' export * from './sync-storage' export * from './types' diff --git a/packages/repo/src/storage/memory-blockstore.ts b/packages/repo/src/storage/memory-blockstore.ts index cb8c32a8c75..5f91311c345 100644 --- a/packages/repo/src/storage/memory-blockstore.ts +++ b/packages/repo/src/storage/memory-blockstore.ts @@ -1,15 +1,15 @@ import { CID } from 'multiformats/cid' -import { CommitData, def, RebaseData } from '../types' +import { CommitData } from '../types' import BlockMap from '../block-map' -import { MST } from '../mst' -import DataDiff from '../data-diff' -import { MissingCommitBlocksError } from '../error' -import RepoStorage from './repo-storage' -import CidSet from '../cid-set' +import ReadableBlockstore from './readable-blockstore' +import { RepoStorage } from './types' -export class MemoryBlockstore extends RepoStorage { +export class MemoryBlockstore + extends ReadableBlockstore + implements RepoStorage +{ blocks: BlockMap - head: CID | null = null + root: CID | null = null constructor(blocks?: BlockMap) { super() @@ -19,8 +19,8 @@ export class MemoryBlockstore extends RepoStorage { } } - async getHead(): Promise { - return this.head + async getRoot(): Promise { + return this.root } async getBytes(cid: CID): Promise { @@ -43,78 +43,19 @@ export class MemoryBlockstore extends RepoStorage { this.blocks.addMap(blocks) } - async indexCommits(commits: CommitData[]): Promise { - commits.forEach((commit) => { - this.blocks.addMap(commit.blocks) - }) - } - - async updateHead(cid: CID, _prev: CID | null): Promise { - this.head = cid + async updateRoot(cid: CID): Promise { + this.root = cid } async applyCommit(commit: CommitData): Promise { - this.blocks.addMap(commit.blocks) - this.head = commit.commit - } - - async getCommitPath( - latest: CID, - earliest: CID | null, - ): Promise { - let curr: CID | null = latest - const path: CID[] = [] - while (curr !== null) { - path.push(curr) - const commit = await this.readObj(curr, def.commit) - if (!earliest && commit.prev === null) { - return path.reverse() - } else if (earliest && commit.prev.equals(earliest)) { - return path.reverse() - } - curr = commit.prev - } - return null - } - - async getBlocksForCommits( - commits: CID[], - ): Promise<{ [commit: string]: BlockMap }> { - const commitData: { [commit: string]: BlockMap } = {} - let prevData: MST | null = null - for (const commitCid of commits) { - const commit = await this.readObj(commitCid, def.commit) - const data = await MST.load(this, commit.data) - const diff = await DataDiff.of(data, prevData) - const { blocks, missing } = await this.getBlocks([ - commitCid, - ...diff.newCidList(), - ]) - if (missing.length > 0) { - throw new MissingCommitBlocksError(commitCid, missing) - } - commitData[commitCid.toString()] = blocks - prevData = data - } - return commitData - } - - async applyRebase(rebase: RebaseData) { - this.putMany(rebase.blocks) - const allCids = new CidSet([ - ...rebase.preservedCids, - ...rebase.blocks.cids(), - ]) - const toDelete: CID[] = [] - this.blocks.forEach((_bytes, cid) => { - if (!allCids.has(cid)) { - toDelete.push(cid) - } - }) - for (const cid of toDelete) { + this.root = commit.cid + const rmCids = commit.removedCids.toList() + for (const cid of rmCids) { this.blocks.delete(cid) } - await this.updateHead(rebase.commit, null) + commit.newBlocks.forEach((bytes, cid) => { + this.blocks.set(cid, bytes) + }) } async sizeInBytes(): Promise { diff --git a/packages/repo/src/storage/repo-storage.ts b/packages/repo/src/storage/repo-storage.ts deleted file mode 100644 index 93a51eaaab3..00000000000 --- a/packages/repo/src/storage/repo-storage.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { CID } from 'multiformats/cid' -import BlockMap from '../block-map' -import { CommitBlockData, CommitData, RebaseData } from '../types' -import ReadableBlockstore from './readable-blockstore' - -export abstract class RepoStorage extends ReadableBlockstore { - abstract getHead(forUpdate?: boolean): Promise - abstract getCommitPath( - latest: CID, - earliest: CID | null, - ): Promise - abstract getBlocksForCommits( - commits: CID[], - ): Promise<{ [commit: string]: BlockMap }> - - abstract putBlock(cid: CID, block: Uint8Array): Promise - abstract putMany(blocks: BlockMap): Promise - abstract updateHead(cid: CID, prev: CID | null): Promise - abstract indexCommits(commit: CommitData[]): Promise - abstract applyRebase(rebase: RebaseData): Promise - - async applyCommit(commit: CommitData): Promise { - await Promise.all([ - this.indexCommits([commit]), - this.updateHead(commit.commit, commit.prev), - ]) - } - - async getCommits( - latest: CID, - earliest: CID | null, - ): Promise { - const commitPath = await this.getCommitPath(latest, earliest) - if (!commitPath) return null - const blocksByCommit = await this.getBlocksForCommits(commitPath) - return commitPath.map((commit) => ({ - commit, - blocks: blocksByCommit[commit.toString()] || new BlockMap(), - })) - } -} - -export default RepoStorage diff --git a/packages/repo/src/storage/types.ts b/packages/repo/src/storage/types.ts index 49c58225714..804be48cbc8 100644 --- a/packages/repo/src/storage/types.ts +++ b/packages/repo/src/storage/types.ts @@ -1,5 +1,34 @@ import stream from 'stream' import { CID } from 'multiformats/cid' +import { RepoRecord } from '@atproto/lexicon' +import { check } from '@atproto/common' +import BlockMap from '../block-map' +import { CommitData } from '../types' + +export interface RepoStorage { + // Writable + getRoot(): Promise + putBlock(cid: CID, block: Uint8Array, rev: string): Promise + putMany(blocks: BlockMap, rev: string): Promise + updateRoot(cid: CID): Promise + applyCommit(commit: CommitData) + + // Readable + getBytes(cid: CID): Promise + has(cid: CID): Promise + getBlocks(cids: CID[]): Promise<{ blocks: BlockMap; missing: CID[] }> + attemptRead( + cid: CID, + def: check.Def, + ): Promise<{ obj: T; bytes: Uint8Array } | null> + readObjAndBytes( + cid: CID, + def: check.Def, + ): Promise<{ obj: T; bytes: Uint8Array }> + readObj(cid: CID, def: check.Def): Promise + attemptReadRecord(cid: CID): Promise + readRecord(cid: CID): Promise +} export interface BlobStore { putTemp(bytes: Uint8Array | stream.Readable): Promise diff --git a/packages/repo/src/sync/consumer.ts b/packages/repo/src/sync/consumer.ts index abdd40ff71a..08ca98195f2 100644 --- a/packages/repo/src/sync/consumer.ts +++ b/packages/repo/src/sync/consumer.ts @@ -1,140 +1,194 @@ import { CID } from 'multiformats/cid' -import { MemoryBlockstore, RepoStorage } from '../storage' -import Repo from '../repo' -import * as verify from '../verify' +import { MemoryBlockstore, ReadableBlockstore, SyncStorage } from '../storage' +import DataDiff from '../data-diff' +import ReadableRepo from '../readable-repo' import * as util from '../util' -import { CommitData, RepoContents, WriteLog } from '../types' -import CidSet from '../cid-set' -import { MissingBlocksError } from '../error' - -// Checkouts -// ------------- - -export const loadCheckout = async ( - storage: RepoStorage, - repoCar: Uint8Array, - did: string, - signingKey: string, -): Promise<{ root: CID; contents: RepoContents }> => { - const { root, blocks } = await util.readCarWithRoot(repoCar) - const updateStorage = new MemoryBlockstore(blocks) - const checkout = await verify.verifyCheckout( - updateStorage, - root, - did, - signingKey, - ) - - const checkoutBlocks = await updateStorage.getBlocks( - checkout.newCids.toList(), - ) - if (checkoutBlocks.missing.length > 0) { - throw new MissingBlocksError('sync', checkoutBlocks.missing) - } - await Promise.all([ - storage.putMany(checkoutBlocks.blocks), - storage.updateHead(root, null), - ]) +import { RecordClaim, VerifiedDiff, VerifiedRepo } from '../types' +import { def } from '../types' +import { MST } from '../mst' +import { cidForCbor } from '@atproto/common' +import BlockMap from '../block-map' + +export const verifyRepoCar = async ( + carBytes: Uint8Array, + did?: string, + signingKey?: string, +): Promise => { + const car = await util.readCarWithRoot(carBytes) + return verifyRepo(car.blocks, car.root, did, signingKey) +} +export const verifyRepo = async ( + blocks: BlockMap, + head: CID, + did?: string, + signingKey?: string, +): Promise => { + const diff = await verifyDiff(null, blocks, head, did, signingKey) + const creates = util.ensureCreates(diff.writes) return { - root, - contents: checkout.contents, + creates, + commit: diff.commit, } } -// Diffs -// ------------- +export const verifyDiffCar = async ( + repo: ReadableRepo | null, + carBytes: Uint8Array, + did?: string, + signingKey?: string, +): Promise => { + const car = await util.readCarWithRoot(carBytes) + return verifyDiff(repo, car.blocks, car.root, did, signingKey) +} -export const loadFullRepo = async ( - storage: RepoStorage, - repoCar: Uint8Array, - did: string, - signingKey: string, -): Promise<{ root: CID; writeLog: WriteLog; repo: Repo }> => { - const { root, blocks } = await util.readCarWithRoot(repoCar) - const updateStorage = new MemoryBlockstore(blocks) - const updates = await verify.verifyFullHistory( +export const verifyDiff = async ( + repo: ReadableRepo | null, + updateBlocks: BlockMap, + updateRoot: CID, + did?: string, + signingKey?: string, +): Promise => { + const stagedStorage = new MemoryBlockstore(updateBlocks) + const updateStorage = repo + ? new SyncStorage(stagedStorage, repo.storage) + : stagedStorage + const updated = await verifyRepoRoot( updateStorage, - root, + updateRoot, did, signingKey, ) - - const [writeLog] = await Promise.all([ - persistUpdates(storage, updateStorage, updates), - storage.updateHead(root, null), - ]) - - const repo = await Repo.load(storage, root) - + const diff = await DataDiff.of(updated.data, repo?.data ?? null) + const writes = await util.diffToWriteDescripts(diff, updateBlocks) + const newBlocks = diff.newMstBlocks + const leaves = updateBlocks.getMany(diff.newLeafCids.toList()) + if (leaves.missing.length > 0) { + throw new Error(`missing leaf blocks: ${leaves.missing}`) + } + newBlocks.addMap(leaves.blocks) + const removedCids = diff.removedCids + const commitCid = await newBlocks.add(updated.commit) + // ensure the commit cid actually changed + if (repo) { + if (commitCid.equals(repo.cid)) { + newBlocks.delete(commitCid) + } else { + removedCids.add(repo.cid) + } + } return { - root, - writeLog, - repo, + writes, + commit: { + cid: updated.cid, + rev: updated.commit.rev, + prev: repo?.cid ?? null, + since: repo?.commit.rev ?? null, + newBlocks, + removedCids, + }, } } -export const loadDiff = async ( - repo: Repo, - diffCar: Uint8Array, - did: string, - signingKey: string, -): Promise<{ root: CID; writeLog: WriteLog }> => { - const { root, blocks } = await util.readCarWithRoot(diffCar) - const updateStorage = new MemoryBlockstore(blocks) - const updates = await verify.verifyUpdates( - repo, - updateStorage, - root, - did, - signingKey, - ) - - const [writeLog] = await Promise.all([ - persistUpdates(repo.storage, updateStorage, updates), - repo.storage.updateHead(root, repo.cid), - ]) - - return { - root, - writeLog, +// @NOTE only verifies the root, not the repo contents +const verifyRepoRoot = async ( + storage: ReadableBlockstore, + head: CID, + did?: string, + signingKey?: string, +): Promise => { + const repo = await ReadableRepo.load(storage, head) + if (did !== undefined && repo.did !== did) { + throw new RepoVerificationError(`Invalid repo did: ${repo.did}`) } + if (signingKey !== undefined) { + const validSig = await util.verifyCommitSig(repo.commit, signingKey) + if (!validSig) { + throw new RepoVerificationError( + `Invalid signature on commit: ${repo.cid.toString()}`, + ) + } + } + return repo } -// Helpers -// ------------- - -export const persistUpdates = async ( - storage: RepoStorage, - updateStorage: RepoStorage, - updates: verify.VerifiedUpdate[], -): Promise => { - const newCids = new CidSet() - for (const update of updates) { - newCids.addSet(update.newCids) +export const verifyProofs = async ( + proofs: Uint8Array, + claims: RecordClaim[], + did: string, + didKey: string, +): Promise<{ verified: RecordClaim[]; unverified: RecordClaim[] }> => { + const car = await util.readCarWithRoot(proofs) + const blockstore = new MemoryBlockstore(car.blocks) + const commit = await blockstore.readObj(car.root, def.commit) + if (commit.did !== did) { + throw new RepoVerificationError(`Invalid repo did: ${commit.did}`) } - - const diffBlocks = await updateStorage.getBlocks(newCids.toList()) - if (diffBlocks.missing.length > 0) { - throw new MissingBlocksError('sync', diffBlocks.missing) + const validSig = await util.verifyCommitSig(commit, didKey) + if (!validSig) { + throw new RepoVerificationError( + `Invalid signature on commit: ${car.root.toString()}`, + ) } - const commits: CommitData[] = updates.map((update) => { - const forCommit = diffBlocks.blocks.getMany(update.newCids.toList()) - if (forCommit.missing.length > 0) { - throw new MissingBlocksError('sync', forCommit.missing) - } - return { - commit: update.commit, - prev: update.prev, - blocks: forCommit.blocks, + const mst = MST.load(blockstore, commit.data) + const verified: RecordClaim[] = [] + const unverified: RecordClaim[] = [] + for (const claim of claims) { + const found = await mst.get( + util.formatDataKey(claim.collection, claim.rkey), + ) + const record = found ? await blockstore.readObj(found, def.map) : null + if (claim.record === null) { + if (record === null) { + verified.push(claim) + } else { + unverified.push(claim) + } + } else { + const expected = await cidForCbor(claim.record) + if (expected.equals(found)) { + verified.push(claim) + } else { + unverified.push(claim) + } } - }) - - await storage.indexCommits(commits) + } + return { verified, unverified } +} - return Promise.all( - updates.map((upd) => - util.diffToWriteDescripts(upd.diff, diffBlocks.blocks), - ), - ) +export const verifyRecords = async ( + proofs: Uint8Array, + did: string, + signingKey: string, +): Promise => { + const car = await util.readCarWithRoot(proofs) + const blockstore = new MemoryBlockstore(car.blocks) + const commit = await blockstore.readObj(car.root, def.commit) + if (commit.did !== did) { + throw new RepoVerificationError(`Invalid repo did: ${commit.did}`) + } + const validSig = await util.verifyCommitSig(commit, signingKey) + if (!validSig) { + throw new RepoVerificationError( + `Invalid signature on commit: ${car.root.toString()}`, + ) + } + const mst = MST.load(blockstore, commit.data) + + const records: RecordClaim[] = [] + const leaves = await mst.reachableLeaves() + for (const leaf of leaves) { + const { collection, rkey } = util.parseDataKey(leaf.key) + const record = await blockstore.attemptReadRecord(leaf.value) + if (record) { + records.push({ + collection, + rkey, + record, + }) + } + } + return records } + +export class RepoVerificationError extends Error {} diff --git a/packages/repo/src/sync/provider.ts b/packages/repo/src/sync/provider.ts index 7fb2dc4abf7..ef6a586b15a 100644 --- a/packages/repo/src/sync/provider.ts +++ b/packages/repo/src/sync/provider.ts @@ -7,10 +7,10 @@ import { RepoStorage } from '../storage' import * as util from '../util' import { MST } from '../mst' -// Checkouts +// Full Repo // ------------- -export const getCheckout = ( +export const getFullRepo = ( storage: RepoStorage, commitCid: CID, ): AsyncIterable => { @@ -22,44 +22,6 @@ export const getCheckout = ( }) } -// Commits -// ------------- - -export const getCommits = ( - storage: RepoStorage, - latest: CID, - earliest: CID | null, -): AsyncIterable => { - return util.writeCar(latest, (car: BlockWriter) => { - return writeCommitsToCarStream(storage, car, latest, earliest) - }) -} - -export const getFullRepo = ( - storage: RepoStorage, - cid: CID, -): AsyncIterable => { - return getCommits(storage, cid, null) -} - -export const writeCommitsToCarStream = async ( - storage: RepoStorage, - car: BlockWriter, - latest: CID, - earliest: CID | null, -): Promise => { - const commits = await storage.getCommits(latest, earliest) - if (commits === null) { - throw new Error('Could not find shared history') - } - if (commits.length === 0) return - for (const commit of commits) { - for (const entry of commit.blocks.entries()) { - await car.put(entry) - } - } -} - // Narrow slices // ------------- diff --git a/packages/repo/src/types.ts b/packages/repo/src/types.ts index 3cc3e8801ab..e3523a3ba87 100644 --- a/packages/repo/src/types.ts +++ b/packages/repo/src/types.ts @@ -3,32 +3,52 @@ import { schema as common, def as commonDef } from '@atproto/common' import { CID } from 'multiformats' import BlockMap from './block-map' import { RepoRecord } from '@atproto/lexicon' +import CidSet from './cid-set' // Repo nodes // --------------- const unsignedCommit = z.object({ did: z.string(), - version: z.number(), - prev: common.cid.nullable(), + version: z.literal(3), data: common.cid, - rev: z.string().optional(), + rev: z.string(), + // `prev` added for backwards compatibility with v2, no requirement of keeping around history + prev: common.cid.nullable().optional(), }) export type UnsignedCommit = z.infer & { sig?: never } const commit = z.object({ did: z.string(), - version: z.number(), - prev: common.cid.nullable(), + version: z.literal(3), data: common.cid, - rev: z.string().optional(), + rev: z.string(), + prev: common.cid.nullable().optional(), sig: common.bytes, }) export type Commit = z.infer +const legacyV2Commit = z.object({ + did: z.string(), + version: z.literal(2), + data: common.cid, + rev: z.string().optional(), + prev: common.cid.nullable(), + sig: common.bytes, +}) +export type LegacyV2Commit = z.infer + +const versionedCommit = z.discriminatedUnion('version', [ + commit, + legacyV2Commit, +]) +export type VersionedCommit = z.infer + export const schema = { ...common, commit, + legacyV2Commit, + versionedCommit, } export const def = { @@ -37,6 +57,10 @@ export const def = { name: 'commit', schema: schema.commit, }, + versionedCommit: { + name: 'versioned_commit', + schema: schema.versionedCommit, + }, } // Repo Operations @@ -93,27 +117,13 @@ export type WriteLog = RecordWriteDescript[][] // Updates/Commits // --------------- -export type CommitBlockData = { - commit: CID - blocks: BlockMap -} - -export type CommitData = CommitBlockData & { - prev: CID | null - rev?: string -} - -export type RebaseData = { - commit: CID - rebased: CID - blocks: BlockMap - preservedCids: CID[] -} - -export type CommitCidData = { - commit: CID +export type CommitData = { + cid: CID + rev: string + since: string | null prev: CID | null - cids: CID[] + newBlocks: BlockMap + removedCids: CidSet } export type RepoUpdate = CommitData & { @@ -139,3 +149,16 @@ export type RecordClaim = { rkey: string record: RepoRecord | null } + +// Sync +// --------------- + +export type VerifiedDiff = { + writes: RecordWriteDescript[] + commit: CommitData +} + +export type VerifiedRepo = { + creates: RecordCreateDescript[] + commit: CommitData +} diff --git a/packages/repo/src/util.ts b/packages/repo/src/util.ts index 42a59f6ea82..563a848d4ae 100644 --- a/packages/repo/src/util.ts +++ b/packages/repo/src/util.ts @@ -11,27 +11,24 @@ import { schema, cidForCbor, byteIterableToStream, + TID, } from '@atproto/common' import { ipldToLex, lexToIpld, LexValue, RepoRecord } from '@atproto/lexicon' import * as crypto from '@atproto/crypto' -import Repo from './repo' -import { MST } from './mst' import DataDiff from './data-diff' -import { RepoStorage } from './storage' import { Commit, + LegacyV2Commit, RecordCreateDescript, RecordDeleteDescript, RecordPath, RecordUpdateDescript, RecordWriteDescript, UnsignedCommit, - WriteLog, WriteOpAction, } from './types' import BlockMap from './block-map' -import { MissingBlocksError } from './error' import * as parse from './parse' import { Keypair } from '@atproto/crypto' import { Readable } from 'stream' @@ -120,33 +117,6 @@ export const readCarWithRoot = async ( } } -export const getWriteLog = async ( - storage: RepoStorage, - latest: CID, - earliest: CID | null, -): Promise => { - const commits = await storage.getCommitPath(latest, earliest) - if (!commits) throw new Error('Could not find shared history') - const heads = await Promise.all(commits.map((c) => Repo.load(storage, c))) - // Turn commit path into list of diffs - let prev = await MST.create(storage) // Empty - const msts = heads.map((h) => h.data) - const diffs: DataDiff[] = [] - for (const mst of msts) { - diffs.push(await DataDiff.of(mst, prev)) - prev = mst - } - const fullDiff = collapseDiffs(diffs) - const diffBlocks = await storage.getBlocks(fullDiff.newCidList()) - if (diffBlocks.missing.length > 0) { - throw new MissingBlocksError('write op log', diffBlocks.missing) - } - // Map MST diffs to write ops - return Promise.all( - diffs.map((diff) => diffToWriteDescripts(diff, diffBlocks.blocks)), - ) -} - export const diffToWriteDescripts = ( diff: DataDiff, blocks: BlockMap, @@ -187,55 +157,18 @@ export const diffToWriteDescripts = ( ]) } -export const collapseWriteLog = (log: WriteLog): RecordWriteDescript[] => { - const creates: Record = {} - const updates: Record = {} - const deletes: Record = {} - for (const commit of log) { - for (const op of commit) { - const key = op.collection + '/' + op.rkey - if (op.action === WriteOpAction.Create) { - const del = deletes[key] - if (del) { - if (del.cid !== op.cid) { - updates[key] = { - ...op, - action: WriteOpAction.Update, - prev: del.cid, - } - } - delete deletes[key] - } else { - creates[key] = op - } - } else if (op.action === WriteOpAction.Update) { - updates[key] = op - delete creates[key] - delete deletes[key] - } else if (op.action === WriteOpAction.Delete) { - if (creates[key]) { - delete creates[key] - } else { - delete updates[key] - deletes[key] = op - } - } else { - throw new Error(`unknown action: ${op}`) - } +export const ensureCreates = ( + descripts: RecordWriteDescript[], +): RecordCreateDescript[] => { + const creates: RecordCreateDescript[] = [] + for (const descript of descripts) { + if (descript.action !== WriteOpAction.Create) { + throw new Error(`Unexpected action: ${descript.action}`) + } else { + creates.push(descript) } } - return [ - ...Object.values(creates), - ...Object.values(updates), - ...Object.values(deletes), - ] -} - -export const collapseDiffs = (diffs: DataDiff[]): DataDiff => { - return diffs.reduce((acc, cur) => { - acc.addDiff(cur) - return acc - }, new DataDiff()) + return creates } export const parseDataKey = (key: string): RecordPath => { @@ -288,3 +221,15 @@ export const cborToLexRecord = (val: Uint8Array): RepoRecord => { export const cidForRecord = async (val: LexValue) => { return cidForCbor(lexToIpld(val)) } + +export const ensureV3Commit = (commit: LegacyV2Commit | Commit): Commit => { + if (commit.version === 3) { + return commit + } else { + return { + ...commit, + version: 3, + rev: commit.rev ?? TID.nextStr(), + } + } +} diff --git a/packages/repo/src/verify.ts b/packages/repo/src/verify.ts deleted file mode 100644 index 28400a957bb..00000000000 --- a/packages/repo/src/verify.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { CID } from 'multiformats/cid' -import { MemoryBlockstore, ReadableBlockstore, RepoStorage } from './storage' -import DataDiff from './data-diff' -import SyncStorage from './storage/sync-storage' -import ReadableRepo from './readable-repo' -import Repo from './repo' -import CidSet from './cid-set' -import * as util from './util' -import { RecordClaim, RepoContents, RepoContentsWithCids } from './types' -import { def } from './types' -import { MST } from './mst' -import { cidForCbor } from '@atproto/common' - -export type VerifiedCheckout = { - contents: RepoContents - newCids: CidSet -} - -export type VerifiedCheckoutWithCids = { - commit: CID - contents: RepoContentsWithCids -} - -export const verifyCheckout = async ( - storage: ReadableBlockstore, - head: CID, - did: string, - signingKey: string, -): Promise => { - const repo = await verifyRepoRoot(storage, head, did, signingKey) - const diff = await DataDiff.of(repo.data, null) - const newCids = new CidSet([repo.cid]).addSet(diff.newCids) - - const contents: RepoContents = {} - for (const add of diff.addList()) { - const { collection, rkey } = util.parseDataKey(add.key) - if (!contents[collection]) { - contents[collection] = {} - } - const record = await storage.readRecord(add.cid) - contents[collection][rkey] = record - } - - return { - contents, - newCids, - } -} - -export const verifyCheckoutWithCids = async ( - storage: ReadableBlockstore, - head: CID, - did: string, - signingKey: string, -): Promise => { - const repo = await verifyRepoRoot(storage, head, did, signingKey) - const diff = await DataDiff.of(repo.data, null) - - const contents: RepoContentsWithCids = {} - for (const add of diff.addList()) { - const { collection, rkey } = util.parseDataKey(add.key) - contents[collection] ??= {} - contents[collection][rkey] = { - cid: add.cid, - value: await storage.readRecord(add.cid), - } - } - - return { - commit: repo.cid, - contents, - } -} - -// @NOTE only verifies the root, not the repo contents -const verifyRepoRoot = async ( - storage: ReadableBlockstore, - head: CID, - did: string, - signingKey: string, -): Promise => { - const repo = await ReadableRepo.load(storage, head) - if (repo.did !== did) { - throw new RepoVerificationError(`Invalid repo did: ${repo.did}`) - } - const validSig = await util.verifyCommitSig(repo.commit, signingKey) - if (!validSig) { - throw new RepoVerificationError( - `Invalid signature on commit: ${repo.cid.toString()}`, - ) - } - return repo -} - -export type VerifiedUpdate = { - commit: CID - prev: CID | null - diff: DataDiff - newCids: CidSet -} - -export const verifyFullHistory = async ( - storage: RepoStorage, - head: CID, - did: string, - signingKey: string, -): Promise => { - const commitPath = await storage.getCommitPath(head, null) - if (commitPath === null) { - throw new RepoVerificationError('Could not find shared history') - } else if (commitPath.length < 1) { - throw new RepoVerificationError('Expected at least one commit') - } - const baseRepo = await Repo.load(storage, commitPath[0]) - const baseDiff = await DataDiff.of(baseRepo.data, null) - const baseRepoCids = new CidSet([baseRepo.cid]).addSet(baseDiff.newCids) - const init: VerifiedUpdate = { - commit: baseRepo.cid, - prev: null, - diff: baseDiff, - newCids: baseRepoCids, - } - const updates = await verifyCommitPath( - baseRepo, - storage, - commitPath.slice(1), - did, - signingKey, - ) - return [init, ...updates] -} - -export const verifyUpdates = async ( - repo: ReadableRepo, - updateStorage: RepoStorage, - updateRoot: CID, - did: string, - signingKey: string, -): Promise => { - const commitPath = await updateStorage.getCommitPath(updateRoot, repo.cid) - if (commitPath === null) { - throw new RepoVerificationError('Could not find shared history') - } - const syncStorage = new SyncStorage(updateStorage, repo.storage) - return verifyCommitPath(repo, syncStorage, commitPath, did, signingKey) -} - -export const verifyCommitPath = async ( - baseRepo: ReadableRepo, - storage: ReadableBlockstore, - commitPath: CID[], - did: string, - signingKey: string, -): Promise => { - const updates: VerifiedUpdate[] = [] - if (commitPath.length === 0) return updates - let prevRepo = baseRepo - for (const commit of commitPath) { - const nextRepo = await ReadableRepo.load(storage, commit) - const diff = await DataDiff.of(nextRepo.data, prevRepo.data) - - if (nextRepo.did !== did) { - throw new RepoVerificationError(`Invalid repo did: ${nextRepo.did}`) - } - if (!util.metaEqual(nextRepo.commit, prevRepo.commit)) { - throw new RepoVerificationError('Not supported: repo metadata updated') - } - - const validSig = await util.verifyCommitSig(nextRepo.commit, signingKey) - if (!validSig) { - throw new RepoVerificationError( - `Invalid signature on commit: ${nextRepo.cid.toString()}`, - ) - } - - const newCids = new CidSet([nextRepo.cid]).addSet(diff.newCids) - - updates.push({ - commit: nextRepo.cid, - prev: prevRepo.cid, - diff, - newCids, - }) - prevRepo = nextRepo - } - return updates -} - -export const verifyProofs = async ( - proofs: Uint8Array, - claims: RecordClaim[], - did: string, - didKey: string, -): Promise<{ verified: RecordClaim[]; unverified: RecordClaim[] }> => { - const car = await util.readCarWithRoot(proofs) - const blockstore = new MemoryBlockstore(car.blocks) - const commit = await blockstore.readObj(car.root, def.commit) - if (commit.did !== did) { - throw new RepoVerificationError(`Invalid repo did: ${commit.did}`) - } - const validSig = await util.verifyCommitSig(commit, didKey) - if (!validSig) { - throw new RepoVerificationError( - `Invalid signature on commit: ${car.root.toString()}`, - ) - } - const mst = MST.load(blockstore, commit.data) - const verified: RecordClaim[] = [] - const unverified: RecordClaim[] = [] - for (const claim of claims) { - const found = await mst.get( - util.formatDataKey(claim.collection, claim.rkey), - ) - const record = found ? await blockstore.readObj(found, def.map) : null - if (claim.record === null) { - if (record === null) { - verified.push(claim) - } else { - unverified.push(claim) - } - } else { - const expected = await cidForCbor(claim.record) - if (expected.equals(found)) { - verified.push(claim) - } else { - unverified.push(claim) - } - } - } - return { verified, unverified } -} - -export const verifyRecords = async ( - proofs: Uint8Array, - did: string, - signingKey: string, -): Promise => { - const car = await util.readCarWithRoot(proofs) - const blockstore = new MemoryBlockstore(car.blocks) - const commit = await blockstore.readObj(car.root, def.commit) - if (commit.did !== did) { - throw new RepoVerificationError(`Invalid repo did: ${commit.did}`) - } - const validSig = await util.verifyCommitSig(commit, signingKey) - if (!validSig) { - throw new RepoVerificationError( - `Invalid signature on commit: ${car.root.toString()}`, - ) - } - const mst = MST.load(blockstore, commit.data) - - const records: RecordClaim[] = [] - const leaves = await mst.reachableLeaves() - for (const leaf of leaves) { - const { collection, rkey } = util.parseDataKey(leaf.key) - const record = await blockstore.attemptReadRecord(leaf.value) - if (record) { - records.push({ - collection, - rkey, - record, - }) - } - } - return records -} - -export class RepoVerificationError extends Error {} diff --git a/packages/repo/tests/_util.ts b/packages/repo/tests/_util.ts index 5e173ba361b..9942cff7568 100644 --- a/packages/repo/tests/_util.ts +++ b/packages/repo/tests/_util.ts @@ -7,15 +7,15 @@ import { RepoStorage } from '../src/storage' import { MST } from '../src/mst' import { BlockMap, - collapseWriteLog, CollectionContents, RecordWriteOp, RepoContents, RecordPath, - WriteLog, WriteOpAction, RecordClaim, Commit, + DataDiff, + CommitData, } from '../src' import { Keypair, randomBytes } from '@atproto/crypto' @@ -109,7 +109,7 @@ export const fillRepo = async ( } } -export const editRepo = async ( +export const formatEdit = async ( repo: Repo, prevData: RepoContents, keypair: crypto.Keypair, @@ -118,91 +118,58 @@ export const editRepo = async ( updates?: number deletes?: number }, -): Promise<{ repo: Repo; data: RepoContents }> => { +): Promise<{ commit: CommitData; data: RepoContents }> => { const { adds = 0, updates = 0, deletes = 0 } = params const repoData: RepoContents = {} + const writes: RecordWriteOp[] = [] for (const collName of testCollections) { - const collData = prevData[collName] + const collData = { ...(prevData[collName] ?? {}) } const shuffled = shuffle(Object.entries(collData)) for (let i = 0; i < adds; i++) { const object = generateObject() const rkey = TID.nextStr() collData[rkey] = object - repo = await repo.applyWrites( - { - action: WriteOpAction.Create, - collection: collName, - rkey, - record: object, - }, - keypair, - ) + writes.push({ + action: WriteOpAction.Create, + collection: collName, + rkey, + record: object, + }) } const toUpdate = shuffled.slice(0, updates) for (let i = 0; i < toUpdate.length; i++) { const object = generateObject() const rkey = toUpdate[i][0] - repo = await repo.applyWrites( - { - action: WriteOpAction.Update, - collection: collName, - rkey, - record: object, - }, - keypair, - ) collData[rkey] = object + writes.push({ + action: WriteOpAction.Update, + collection: collName, + rkey, + record: object, + }) } const toDelete = shuffled.slice(updates, deletes) for (let i = 0; i < toDelete.length; i++) { const rkey = toDelete[i][0] - repo = await repo.applyWrites( - { - action: WriteOpAction.Delete, - collection: collName, - rkey, - }, - keypair, - ) delete collData[rkey] + writes.push({ + action: WriteOpAction.Delete, + collection: collName, + rkey, + }) } repoData[collName] = collData } + const commit = await repo.formatCommit(writes, keypair) return { - repo, + commit, data: repoData, } } -export const verifyRepoDiff = async ( - writeLog: WriteLog, - before: RepoContents, - after: RepoContents, -): Promise => { - const getVal = (op: RecordWriteOp, data: RepoContents) => { - return (data[op.collection] || {})[op.rkey] - } - const ops = await collapseWriteLog(writeLog) - - for (const op of ops) { - if (op.action === WriteOpAction.Create) { - expect(getVal(op, before)).toBeUndefined() - expect(getVal(op, after)).toEqual(op.record) - } else if (op.action === WriteOpAction.Update) { - expect(getVal(op, before)).toBeDefined() - expect(getVal(op, after)).toEqual(op.record) - } else if (op.action === WriteOpAction.Delete) { - expect(getVal(op, before)).toBeDefined() - expect(getVal(op, after)).toBeUndefined() - } else { - throw new Error('unexpected op type') - } - } -} - export const contentsToClaims = (contents: RepoContents): RecordClaim[] => { const claims: RecordClaim[] = [] for (const coll of Object.keys(contents)) { @@ -233,23 +200,27 @@ export const addBadCommit = async ( keypair: Keypair, ): Promise => { const obj = generateObject() - const blocks = new BlockMap() - const cid = await blocks.add(obj) + const newBlocks = new BlockMap() + const cid = await newBlocks.add(obj) const updatedData = await repo.data.add(`com.example.test/${TID.next()}`, cid) - const unstoredData = await updatedData.getUnstoredBlocks() - blocks.addMap(unstoredData.blocks) + const dataCid = await updatedData.getPointer() + const diff = await DataDiff.of(updatedData, repo.data) + newBlocks.addMap(diff.newMstBlocks) // we generate a bad sig by signing some other data + const rev = TID.nextStr(repo.commit.rev) const commit: Commit = { ...repo.commit, - prev: repo.cid, - data: unstoredData.root, + rev, + data: dataCid, sig: await keypair.sign(randomBytes(256)), } - const commitCid = await blocks.add(commit) + const commitCid = await newBlocks.add(commit) await repo.storage.applyCommit({ - commit: commitCid, + cid: commitCid, + rev, prev: repo.cid, - blocks: blocks, + newBlocks, + removedCids: diff.removedCids, }) return await Repo.load(repo.storage, commitCid) } diff --git a/packages/repo/tests/mst.test.ts b/packages/repo/tests/mst.test.ts index 109c6538ffa..5a39d977417 100644 --- a/packages/repo/tests/mst.test.ts +++ b/packages/repo/tests/mst.test.ts @@ -150,7 +150,10 @@ describe('Merkle Search Tree', () => { } else { cid = entry.value } - const found = (await blockstore.has(cid)) || diff.newCids.has(cid) + const found = + (await blockstore.has(cid)) || + diff.newMstBlocks.has(cid) || + diff.newLeafCids.has(cid) expect(found).toBeTruthy() } }) diff --git a/packages/repo/tests/sync/narrow.test.ts b/packages/repo/tests/proofs.test.ts similarity index 80% rename from packages/repo/tests/sync/narrow.test.ts rename to packages/repo/tests/proofs.test.ts index 1bdeb10b0f2..9591c07131a 100644 --- a/packages/repo/tests/sync/narrow.test.ts +++ b/packages/repo/tests/proofs.test.ts @@ -1,13 +1,12 @@ import { TID, streamToBuffer } from '@atproto/common' import * as crypto from '@atproto/crypto' -import { RecordClaim, Repo, RepoContents } from '../../src' -import { MemoryBlockstore } from '../../src/storage' -import * as verify from '../../src/verify' -import * as sync from '../../src/sync' +import { RecordClaim, Repo, RepoContents } from '../src' +import { MemoryBlockstore } from '../src/storage' +import * as sync from '../src/sync' -import * as util from '../_util' +import * as util from './_util' -describe('Narrow Sync', () => { +describe('Repo Proofs', () => { let storage: MemoryBlockstore let repo: Repo let keypair: crypto.Keypair @@ -29,7 +28,7 @@ describe('Narrow Sync', () => { } const doVerify = (proofs: Uint8Array, claims: RecordClaim[]) => { - return verify.verifyProofs(proofs, claims, repoDid, keypair.did()) + return sync.verifyProofs(proofs, claims, repoDid, keypair.did()) } it('verifies valid records', async () => { @@ -112,7 +111,7 @@ describe('Narrow Sync', () => { possible[8], ] const proofs = await getProofs(claims) - const records = await verify.verifyRecords(proofs, repoDid, keypair.did()) + const records = await sync.verifyRecords(proofs, repoDid, keypair.did()) for (const record of records) { const foundClaim = claims.find( (claim) => @@ -127,23 +126,13 @@ describe('Narrow Sync', () => { } }) - it('verifyRecords throws on a bad signature', async () => { - const badRepo = await util.addBadCommit(repo, keypair) - const claims = util.contentsToClaims(repoData) - const proofs = await streamToBuffer( - sync.getRecords(storage, badRepo.cid, claims), - ) - const fn = verify.verifyRecords(proofs, repoDid, keypair.did()) - await expect(fn).rejects.toThrow(verify.RepoVerificationError) - }) - it('verifyProofs throws on a bad signature', async () => { const badRepo = await util.addBadCommit(repo, keypair) const claims = util.contentsToClaims(repoData) const proofs = await streamToBuffer( sync.getRecords(storage, badRepo.cid, claims), ) - const fn = verify.verifyProofs(proofs, claims, repoDid, keypair.did()) - await expect(fn).rejects.toThrow(verify.RepoVerificationError) + const fn = sync.verifyProofs(proofs, claims, repoDid, keypair.did()) + await expect(fn).rejects.toThrow(sync.RepoVerificationError) }) }) diff --git a/packages/repo/tests/rebase.test.ts b/packages/repo/tests/rebase.test.ts deleted file mode 100644 index e5afcb1cec5..00000000000 --- a/packages/repo/tests/rebase.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as crypto from '@atproto/crypto' -import { Repo } from '../src/repo' -import { MemoryBlockstore } from '../src/storage' -import * as util from './_util' -import { Secp256k1Keypair } from '@atproto/crypto' - -describe('Rebases', () => { - let storage: MemoryBlockstore - let keypair: crypto.Keypair - let repo: Repo - - it('fills a repo with data', async () => { - storage = new MemoryBlockstore() - keypair = await Secp256k1Keypair.create() - repo = await Repo.create(storage, keypair.did(), keypair) - const filled = await util.fillRepo(repo, keypair, 100) - repo = filled.repo - }) - - it('rebases the repo & preserves contents', async () => { - const dataCidBefore = await repo.data.getPointer() - const contents = await repo.getContents() - repo = await repo.rebase(keypair) - const rebasedContents = await repo.getContents() - expect(rebasedContents).toEqual(contents) - const dataCidAfter = await repo.data.getPointer() - expect(dataCidAfter.equals(dataCidBefore)).toBeTruthy() - }) - - it('only keeps around relevant cids', async () => { - const allCids = await repo.data.allCids() - allCids.add(repo.cid) - for (const cid of storage.blocks.cids()) { - expect(allCids.has(cid)).toBeTruthy() - } - }) -}) diff --git a/packages/repo/tests/repo.test.ts b/packages/repo/tests/repo.test.ts index c08d0dae23f..75d7ef23a14 100644 --- a/packages/repo/tests/repo.test.ts +++ b/packages/repo/tests/repo.test.ts @@ -22,7 +22,7 @@ describe('Repo', () => { it('has proper metadata', async () => { expect(repo.did).toEqual(keypair.did()) - expect(repo.version).toBe(2) + expect(repo.version).toBe(3) }) it('does basic operations', async () => { @@ -75,12 +75,13 @@ describe('Repo', () => { }) it('edits and deletes content', async () => { - const edited = await util.editRepo(repo, repoData, keypair, { + const edit = await util.formatEdit(repo, repoData, keypair, { adds: 20, updates: 20, deletes: 20, }) - repo = edited.repo + repo = await repo.applyCommit(edit.commit) + repoData = edit.data const contents = await repo.getContents() expect(contents).toEqual(repoData) }) @@ -100,6 +101,6 @@ describe('Repo', () => { const contents = await reloadedRepo.getContents() expect(contents).toEqual(repoData) expect(repo.did).toEqual(keypair.did()) - expect(repo.version).toBe(2) + expect(repo.version).toBe(3) }) }) diff --git a/packages/repo/tests/sync.test.ts b/packages/repo/tests/sync.test.ts new file mode 100644 index 00000000000..9c8597a0228 --- /dev/null +++ b/packages/repo/tests/sync.test.ts @@ -0,0 +1,97 @@ +import * as crypto from '@atproto/crypto' +import { + CidSet, + Repo, + RepoContents, + RepoVerificationError, + readCarWithRoot, +} from '../src' +import { MemoryBlockstore } from '../src/storage' +import * as sync from '../src/sync' + +import * as util from './_util' +import { streamToBuffer } from '@atproto/common' +import { CarReader } from '@ipld/car/reader' + +describe('Repo Sync', () => { + let storage: MemoryBlockstore + let repo: Repo + let keypair: crypto.Keypair + let repoData: RepoContents + + const repoDid = 'did:example:test' + + beforeAll(async () => { + storage = new MemoryBlockstore() + keypair = await crypto.Secp256k1Keypair.create() + repo = await Repo.create(storage, repoDid, keypair) + const filled = await util.fillRepo(repo, keypair, 20) + repo = filled.repo + repoData = filled.data + }) + + it('sync a full repo', async () => { + const carBytes = await streamToBuffer(sync.getFullRepo(storage, repo.cid)) + const car = await readCarWithRoot(carBytes) + const verified = await sync.verifyRepo( + car.blocks, + car.root, + repoDid, + keypair.did(), + ) + const syncStorage = new MemoryBlockstore() + await syncStorage.applyCommit(verified.commit) + const loadedRepo = await Repo.load(syncStorage, car.root) + const contents = await loadedRepo.getContents() + expect(contents).toEqual(repoData) + const contentsFromOps: RepoContents = {} + for (const write of verified.creates) { + contentsFromOps[write.collection] ??= {} + contentsFromOps[write.collection][write.rkey] = write.record + } + expect(contentsFromOps).toEqual(repoData) + }) + + it('does not sync duplicate blocks', async () => { + const carBytes = await streamToBuffer(sync.getFullRepo(storage, repo.cid)) + const car = await CarReader.fromBytes(carBytes) + const cids = new CidSet() + for await (const block of car.blocks()) { + if (cids.has(block.cid)) { + throw new Error(`duplicate block: :${block.cid.toString()}`) + } + cids.add(block.cid) + } + }) + + it('syncs a repo that is behind', async () => { + // add more to providers's repo & have consumer catch up + const edit = await util.formatEdit(repo, repoData, keypair, { + adds: 10, + updates: 10, + deletes: 10, + }) + const verified = await sync.verifyDiff( + repo, + edit.commit.newBlocks, + edit.commit.cid, + repoDid, + keypair.did(), + ) + await storage.applyCommit(verified.commit) + repo = await Repo.load(storage, verified.commit.cid) + const contents = await repo.getContents() + expect(contents).toEqual(edit.data) + }) + + it('throws on a bad signature', async () => { + const badRepo = await util.addBadCommit(repo, keypair) + const carBytes = await streamToBuffer( + sync.getFullRepo(storage, badRepo.cid), + ) + const car = await readCarWithRoot(carBytes) + await expect( + sync.verifyRepo(car.blocks, car.root, repoDid, keypair.did()), + ).rejects.toThrow(RepoVerificationError) + }) +}) diff --git a/packages/repo/tests/sync/checkout.test.ts b/packages/repo/tests/sync/checkout.test.ts deleted file mode 100644 index df0edec2a70..00000000000 --- a/packages/repo/tests/sync/checkout.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as crypto from '@atproto/crypto' -import { CidSet, Repo, RepoContents, RepoVerificationError } from '../../src' -import { MemoryBlockstore } from '../../src/storage' -import * as sync from '../../src/sync' - -import * as util from '../_util' -import { streamToBuffer } from '@atproto/common' -import { CarReader } from '@ipld/car/reader' - -describe('Checkout Sync', () => { - let storage: MemoryBlockstore - let syncStorage: MemoryBlockstore - let repo: Repo - let keypair: crypto.Keypair - let repoData: RepoContents - - const repoDid = 'did:example:test' - - beforeAll(async () => { - storage = new MemoryBlockstore() - keypair = await crypto.Secp256k1Keypair.create() - repo = await Repo.create(storage, repoDid, keypair) - syncStorage = new MemoryBlockstore() - const filled = await util.fillRepo(repo, keypair, 20) - repo = filled.repo - repoData = filled.data - }) - - it('sync a non-historical repo checkout', async () => { - const checkoutCar = await streamToBuffer( - sync.getCheckout(storage, repo.cid), - ) - const checkout = await sync.loadCheckout( - syncStorage, - checkoutCar, - repoDid, - keypair.did(), - ) - const checkoutRepo = await Repo.load(syncStorage, checkout.root) - const contents = await checkoutRepo.getContents() - expect(contents).toEqual(repoData) - expect(checkout.contents).toEqual(repoData) - }) - - it('does not sync unneeded blocks during checkout', async () => { - const commitPath = await storage.getCommitPath(repo.cid, null) - if (!commitPath) { - throw new Error('Could not get commitPath') - } - const hasGenesisCommit = await syncStorage.has(commitPath[0]) - expect(hasGenesisCommit).toBeFalsy() - }) - - it('does not sync duplicate blocks', async () => { - const carBytes = await streamToBuffer(sync.getCheckout(storage, repo.cid)) - const car = await CarReader.fromBytes(carBytes) - const cids = new CidSet() - for await (const block of car.blocks()) { - if (cids.has(block.cid)) { - throw new Error(`duplicate block: :${block.cid.toString()}`) - } - cids.add(block.cid) - } - }) - - it('throws on a bad signature', async () => { - const badRepo = await util.addBadCommit(repo, keypair) - const checkoutCar = await streamToBuffer( - sync.getCheckout(storage, badRepo.cid), - ) - await expect( - sync.loadCheckout(syncStorage, checkoutCar, repoDid, keypair.did()), - ).rejects.toThrow(RepoVerificationError) - }) -}) diff --git a/packages/repo/tests/sync/diff.test.ts b/packages/repo/tests/sync/diff.test.ts deleted file mode 100644 index c686ec7142c..00000000000 --- a/packages/repo/tests/sync/diff.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import * as crypto from '@atproto/crypto' -import { Repo, RepoContents } from '../../src' -import { MemoryBlockstore } from '../../src/storage' -import * as sync from '../../src/sync' - -import * as util from '../_util' -import { streamToBuffer } from '@atproto/common' - -describe('Diff Sync', () => { - let storage: MemoryBlockstore - let syncStorage: MemoryBlockstore - let repo: Repo - let keypair: crypto.Keypair - let repoData: RepoContents - - const repoDid = 'did:example:test' - - beforeAll(async () => { - storage = new MemoryBlockstore() - keypair = await crypto.Secp256k1Keypair.create() - repo = await Repo.create(storage, repoDid, keypair) - syncStorage = new MemoryBlockstore() - }) - - let syncRepo: Repo - - it('syncs an empty repo', async () => { - const car = await streamToBuffer(sync.getFullRepo(storage, repo.cid)) - const loaded = await sync.loadFullRepo( - syncStorage, - car, - repoDid, - keypair.did(), - ) - syncRepo = await Repo.load(syncStorage, loaded.root) - const data = await syncRepo.data.list(10) - expect(data.length).toBe(0) - }) - - it('syncs a repo that is starting from scratch', async () => { - const filled = await util.fillRepo(repo, keypair, 100) - repo = filled.repo - repoData = filled.data - - const car = await streamToBuffer(sync.getFullRepo(storage, repo.cid)) - const loaded = await sync.loadFullRepo( - syncStorage, - car, - repoDid, - keypair.did(), - ) - syncRepo = await Repo.load(syncStorage, loaded.root) - const contents = await syncRepo.getContents() - expect(contents).toEqual(repoData) - await util.verifyRepoDiff(loaded.writeLog, {}, repoData) - }) - - it('syncs a repo that is behind', async () => { - // add more to providers's repo & have consumer catch up - const beforeData = structuredClone(repoData) - const edited = await util.editRepo(repo, repoData, keypair, { - adds: 20, - updates: 20, - deletes: 20, - }) - repo = edited.repo - repoData = edited.data - const diffCar = await streamToBuffer( - sync.getCommits(storage, repo.cid, syncRepo.cid), - ) - const loaded = await sync.loadDiff( - syncRepo, - diffCar, - repoDid, - keypair.did(), - ) - syncRepo = await Repo.load(syncStorage, loaded.root) - const contents = await syncRepo.getContents() - expect(contents).toEqual(repoData) - await util.verifyRepoDiff(loaded.writeLog, beforeData, repoData) - }) - - it('throws on a bad signature', async () => { - const badRepo = await util.addBadCommit(repo, keypair) - const diffCar = await streamToBuffer( - sync.getCommits(storage, badRepo.cid, syncRepo.cid), - ) - await expect( - sync.loadDiff(syncRepo, diffCar, repoDid, keypair.did()), - ).rejects.toThrow('Invalid signature on commit') - }) -}) From 2b8cbcee1f92f484d2f95ed786bef0beeac1bf40 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 29 Aug 2023 17:09:46 -0700 Subject: [PATCH 204/237] Add preferences helpers to SDK (#1537) --- packages/api/src/bsky-agent.ts | 180 ++++++++++++++++++++++++++ packages/api/src/types.ts | 20 +++ packages/api/tests/bsky-agent.test.ts | 149 +++++++++++++++++++++ 3 files changed, 349 insertions(+) diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 6122934d23e..29fedfa2122 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -3,8 +3,10 @@ import { AtpAgent } from './agent' import { AppBskyFeedPost, AppBskyActorProfile, + AppBskyActorDefs, ComAtprotoRepoPutRecord, } from './client' +import { BskyPreferences, BskyLabelPreference } from './types' export class BskyAgent extends AtpAgent { get app() { @@ -236,4 +238,182 @@ export class BskyAgent extends AtpAgent { seenAt, }) } + + async getPreferences(): Promise { + const prefs: BskyPreferences = { + feeds: { + saved: undefined, + pinned: undefined, + }, + adultContentEnabled: false, + contentLabels: {}, + } + const res = await this.app.bsky.actor.getPreferences({}) + for (const pref of res.data.preferences) { + if ( + AppBskyActorDefs.isAdultContentPref(pref) && + AppBskyActorDefs.validateAdultContentPref(pref).success + ) { + prefs.adultContentEnabled = pref.enabled + } else if ( + AppBskyActorDefs.isContentLabelPref(pref) && + AppBskyActorDefs.validateAdultContentPref(pref).success + ) { + let value = pref.visibility + if (value === 'show') { + value = 'ignore' + } + if (value === 'ignore' || value === 'warn' || value === 'hide') { + prefs.contentLabels[pref.label] = value as BskyLabelPreference + } + } else if ( + AppBskyActorDefs.isSavedFeedsPref(pref) && + AppBskyActorDefs.validateSavedFeedsPref(pref).success + ) { + prefs.feeds.saved = pref.saved + prefs.feeds.pinned = pref.pinned + } + } + return prefs + } + + async setSavedFeeds(saved: string[], pinned: string[]) { + return updateFeedPreferences(this, () => ({ + saved, + pinned, + })) + } + + async addSavedFeed(v: string) { + return updateFeedPreferences(this, (saved: string[], pinned: string[]) => ({ + saved: [...saved.filter((uri) => uri !== v), v], + pinned, + })) + } + + async removeSavedFeed(v: string) { + return updateFeedPreferences(this, (saved: string[], pinned: string[]) => ({ + saved: saved.filter((uri) => uri !== v), + pinned: pinned.filter((uri) => uri !== v), + })) + } + + async addPinnedFeed(v: string) { + return updateFeedPreferences(this, (saved: string[], pinned: string[]) => ({ + saved: [...saved.filter((uri) => uri !== v), v], + pinned: [...pinned.filter((uri) => uri !== v), v], + })) + } + + async removePinnedFeed(v: string) { + return updateFeedPreferences(this, (saved: string[], pinned: string[]) => ({ + saved, + pinned: pinned.filter((uri) => uri !== v), + })) + } + + async setAdultContentEnabled(v: boolean) { + await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { + const existing = prefs.find( + (pref) => + AppBskyActorDefs.isAdultContentPref(pref) && + AppBskyActorDefs.validateAdultContentPref(pref).success, + ) + if (existing) { + existing.enabled = v + } else { + prefs.push({ + $type: 'app.bsky.actor.defs#adultContentPref', + enabled: v, + }) + } + return prefs + }) + } + + async setContentLabelPref(key: string, value: BskyLabelPreference) { + // TEMP update old value + if (value === 'show') { + value = 'ignore' + } + + await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { + const existing = prefs.find( + (pref) => + AppBskyActorDefs.isContentLabelPref(pref) && + AppBskyActorDefs.validateAdultContentPref(pref).success && + pref.label === key, + ) + if (existing) { + existing.visibility = value + } else { + prefs.push({ + $type: 'app.bsky.actor.defs#contentLabelPref', + label: key, + visibility: value, + }) + } + return prefs + }) + } +} + +/** + * This function updates the preferences of a user and allows for a callback function to be executed + * before the update. + * @param cb - cb is a callback function that takes in a single parameter of type + * AppBskyActorDefs.Preferences and returns either a boolean or void. This callback function is used to + * update the preferences of the user. The function is called with the current preferences as an + * argument and if the callback returns false, the preferences are not updated. + */ +async function updatePreferences( + agent: BskyAgent, + cb: ( + prefs: AppBskyActorDefs.Preferences, + ) => AppBskyActorDefs.Preferences | false, +) { + const res = await agent.app.bsky.actor.getPreferences({}) + const newPrefs = cb(res.data.preferences) + if (newPrefs === false) { + return + } + await agent.app.bsky.actor.putPreferences({ + preferences: newPrefs, + }) +} + +/** + * A helper specifically for updating feed preferences + */ +async function updateFeedPreferences( + agent: BskyAgent, + cb: ( + saved: string[], + pinned: string[], + ) => { saved: string[]; pinned: string[] }, +): Promise<{ saved: string[]; pinned: string[] }> { + let res + await updatePreferences(agent, (prefs: AppBskyActorDefs.Preferences) => { + let feedsPref = prefs.find( + (pref) => + AppBskyActorDefs.isSavedFeedsPref(pref) && + AppBskyActorDefs.validateSavedFeedsPref(pref).success, + ) as AppBskyActorDefs.SavedFeedsPref | undefined + if (feedsPref) { + res = cb(feedsPref.saved, feedsPref.pinned) + feedsPref.saved = res.saved + feedsPref.pinned = res.pinned + } else { + res = cb([], []) + feedsPref = { + $type: 'app.bsky.actor.defs#savedFeedsPref', + saved: res.saved, + pinned: res.pinned, + } + } + return prefs + .filter((pref) => !AppBskyActorDefs.isSavedFeedsPref(pref)) + .concat([feedsPref]) + }) + return res } diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 38ad34388d0..e8597795979 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -1,3 +1,5 @@ +import { LabelPreference } from './moderation/types' + /** * Used by the PersistSessionHandler to indicate what change occurred */ @@ -70,3 +72,21 @@ export type AtpAgentFetchHandler = ( export interface AtpAgentGlobalOpts { fetch: AtpAgentFetchHandler } + +/** + * Content-label preference + */ +export type BskyLabelPreference = LabelPreference | 'show' +// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf + +/** + * Bluesky preferences object + */ +export interface BskyPreferences { + feeds: { + saved?: string[] + pinned?: string[] + } + adultContentEnabled: boolean + contentLabels: Record +} diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index 4876bc5089e..981b192c1d4 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -200,4 +200,153 @@ describe('agent', () => { await expect(agent.deleteFollow('foo')).rejects.toThrow('Not logged in') }) }) + + describe('preferences methods', () => { + it('gets and sets preferences correctly', async () => { + const agent = new BskyAgent({ service: server.url }) + + await agent.createAccount({ + handle: 'user5.test', + email: 'user5@test.com', + password: 'password', + }) + + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { pinned: undefined, saved: undefined }, + adultContentEnabled: false, + contentLabels: {}, + }) + + await agent.setAdultContentEnabled(true) + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { pinned: undefined, saved: undefined }, + adultContentEnabled: true, + contentLabels: {}, + }) + + await agent.setAdultContentEnabled(false) + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { pinned: undefined, saved: undefined }, + adultContentEnabled: false, + contentLabels: {}, + }) + + await agent.setContentLabelPref('impersonation', 'warn') + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { pinned: undefined, saved: undefined }, + adultContentEnabled: false, + contentLabels: { + impersonation: 'warn', + }, + }) + + await agent.setContentLabelPref('spam', 'show') // will convert to 'ignore' + await agent.setContentLabelPref('impersonation', 'hide') + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { pinned: undefined, saved: undefined }, + adultContentEnabled: false, + contentLabels: { + impersonation: 'hide', + spam: 'ignore', + }, + }) + + await agent.addSavedFeed('at://bob.com/app.bsky.feed.generator/fake') + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: [], + saved: ['at://bob.com/app.bsky.feed.generator/fake'], + }, + adultContentEnabled: false, + contentLabels: { + impersonation: 'hide', + spam: 'ignore', + }, + }) + + await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: ['at://bob.com/app.bsky.feed.generator/fake'], + saved: ['at://bob.com/app.bsky.feed.generator/fake'], + }, + adultContentEnabled: false, + contentLabels: { + impersonation: 'hide', + spam: 'ignore', + }, + }) + + await agent.removePinnedFeed('at://bob.com/app.bsky.feed.generator/fake') + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: [], + saved: ['at://bob.com/app.bsky.feed.generator/fake'], + }, + adultContentEnabled: false, + contentLabels: { + impersonation: 'hide', + spam: 'ignore', + }, + }) + + await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake') + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: [], + saved: [], + }, + adultContentEnabled: false, + contentLabels: { + impersonation: 'hide', + spam: 'ignore', + }, + }) + + await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: ['at://bob.com/app.bsky.feed.generator/fake'], + saved: ['at://bob.com/app.bsky.feed.generator/fake'], + }, + adultContentEnabled: false, + contentLabels: { + impersonation: 'hide', + spam: 'ignore', + }, + }) + + await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake2') + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: [ + 'at://bob.com/app.bsky.feed.generator/fake', + 'at://bob.com/app.bsky.feed.generator/fake2', + ], + saved: [ + 'at://bob.com/app.bsky.feed.generator/fake', + 'at://bob.com/app.bsky.feed.generator/fake2', + ], + }, + adultContentEnabled: false, + contentLabels: { + impersonation: 'hide', + spam: 'ignore', + }, + }) + + await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake') + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: ['at://bob.com/app.bsky.feed.generator/fake2'], + saved: ['at://bob.com/app.bsky.feed.generator/fake2'], + }, + adultContentEnabled: false, + contentLabels: { + impersonation: 'hide', + spam: 'ignore', + }, + }) + }) + }) }) From 8db9077c68bad40a7a9347308790817e035fd669 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 29 Aug 2023 17:10:13 -0700 Subject: [PATCH 205/237] @atproto/api@0.6.7 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 5d313b4666d..0f7537c0c21 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.6", + "version": "0.6.7", "main": "src/index.ts", "scripts": { "codegen": "yarn docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From 6d765f7f61fd9b34afa9c963dad044797ec2ad9a Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Tue, 29 Aug 2023 17:13:35 -0700 Subject: [PATCH 206/237] @atproto/api@0.6.8 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 0f7537c0c21..311950384f4 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.7", + "version": "0.6.8", "main": "src/index.ts", "scripts": { "codegen": "yarn docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", From f9d6673da99269d24ef188d3798e80c9f50bdac1 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Wed, 30 Aug 2023 00:13:17 -0400 Subject: [PATCH 207/237] @atproto/repo@0.3.0 --- packages/repo/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/repo/package.json b/packages/repo/package.json index ffda8e60550..e879099c21c 100644 --- a/packages/repo/package.json +++ b/packages/repo/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/repo", - "version": "0.2.0", + "version": "0.3.0", "main": "src/index.ts", "license": "MIT", "repository": { @@ -29,9 +29,9 @@ "dependencies": { "@atproto/common": "*", "@atproto/crypto": "*", - "@atproto/syntax": "*", "@atproto/identity": "*", "@atproto/lexicon": "*", + "@atproto/syntax": "*", "@ipld/car": "^3.2.3", "@ipld/dag-cbor": "^7.0.0", "multiformats": "^9.6.4", From f8c89e54ca95cba10de5d4ba0ccc31898f834cd7 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 31 Aug 2023 11:03:22 -0500 Subject: [PATCH 208/237] Legacy checkouts (#1539) * serve legacy repos & checkouts * tidy * check lock before formatting commit * tidy locks * clean up db test * check error on failed commit format * be more generous with new blocks * Update packages/pds/src/db/index.ts Co-authored-by: devin ivy * force upgrade & ensure lock * make it a bit smarter * add log * dont build branch --------- Co-authored-by: devin ivy --- .../com/atproto/temp/upgradeRepoVersion.json | 3 +- .../atproto/sync/deprecated/getCheckout.ts | 22 +++++---- .../pds/src/api/com/atproto/sync/getRepo.ts | 4 +- .../src/api/com/atproto/upgradeRepoVersion.ts | 31 ++++++++++-- packages/pds/src/db/index.ts | 11 ++++- packages/pds/src/lexicon/lexicons.ts | 3 ++ .../com/atproto/temp/upgradeRepoVersion.ts | 1 + packages/pds/src/services/repo/index.ts | 48 ++++++++++++++++--- packages/pds/src/sql-repo-storage.ts | 46 +++++++++++++++++- packages/pds/tests/db.test.ts | 8 ++-- 10 files changed, 150 insertions(+), 27 deletions(-) diff --git a/lexicons/com/atproto/temp/upgradeRepoVersion.json b/lexicons/com/atproto/temp/upgradeRepoVersion.json index bd19374a859..05d8c7197fd 100644 --- a/lexicons/com/atproto/temp/upgradeRepoVersion.json +++ b/lexicons/com/atproto/temp/upgradeRepoVersion.json @@ -11,7 +11,8 @@ "type": "object", "required": ["did"], "properties": { - "did": { "type": "string", "format": "did" } + "did": { "type": "string", "format": "did" }, + "force": { "type": "boolean" } } } } diff --git a/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts b/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts index cbde7131c66..28e19f22840 100644 --- a/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts +++ b/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts @@ -6,6 +6,7 @@ import SqlRepoStorage, { } from '../../../../../sql-repo-storage' import AppContext from '../../../../../context' import { isUserOrAdmin } from '../../../../../auth' +import { getFullRepo } from '@atproto/repo' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getCheckout({ @@ -23,15 +24,20 @@ export default function (server: Server, ctx: AppContext) { } const storage = new SqlRepoStorage(ctx.db, did) - let carStream: AsyncIterable - try { - carStream = await storage.getCarStream() - } catch (err) { - if (err instanceof RepoRootNotFoundError) { - throw new InvalidRequestError(`Could not find repo for DID: ${did}`) - } - throw err + const head = await storage.getRoot() + if (!head) { + throw new InvalidRequestError(`Could not find repo for DID: ${did}`) } + const carStream = getFullRepo(storage, head) + // let carStream: AsyncIterable + // try { + // carStream = await storage.getCarStream() + // } catch (err) { + // if (err instanceof RepoRootNotFoundError) { + // throw new InvalidRequestError(`Could not find repo for DID: ${did}`) + // } + // throw err + // } return { encoding: 'application/vnd.ipld.car', diff --git a/packages/pds/src/api/com/atproto/sync/getRepo.ts b/packages/pds/src/api/com/atproto/sync/getRepo.ts index 9037a2a3a9c..a88a39cb144 100644 --- a/packages/pds/src/api/com/atproto/sync/getRepo.ts +++ b/packages/pds/src/api/com/atproto/sync/getRepo.ts @@ -25,7 +25,9 @@ export default function (server: Server, ctx: AppContext) { const storage = new SqlRepoStorage(ctx.db, did) let carStream: AsyncIterable try { - carStream = await storage.getCarStream(since) + carStream = since + ? await storage.getCarStream(since) + : await storage.getCarStreamLegacy() } catch (err) { if (err instanceof RepoRootNotFoundError) { throw new InvalidRequestError(`Could not find repo for DID: ${did}`) diff --git a/packages/pds/src/api/com/atproto/upgradeRepoVersion.ts b/packages/pds/src/api/com/atproto/upgradeRepoVersion.ts index ad018d08089..dd20943be3e 100644 --- a/packages/pds/src/api/com/atproto/upgradeRepoVersion.ts +++ b/packages/pds/src/api/com/atproto/upgradeRepoVersion.ts @@ -1,5 +1,5 @@ import { InvalidRequestError } from '@atproto/xrpc-server' -import { TID, chunkArray } from '@atproto/common' +import { TID, chunkArray, wait } from '@atproto/common' import { Server } from '../../../lexicon' import SqlRepoStorage from '../../../sql-repo-storage' import AppContext from '../../../context' @@ -14,6 +14,7 @@ import { } from '@atproto/repo' import { CID } from 'multiformats/cid' import { formatSeqCommit, sequenceEvt } from '../../../sequencer' +import { httpLogger as log } from '../../../logger' export default function (server: Server, ctx: AppContext) { server.com.atproto.temp.upgradeRepoVersion({ @@ -22,11 +23,11 @@ export default function (server: Server, ctx: AppContext) { if (!auth.credentials.admin) { throw new InvalidRequestError('must be admin') } - const { did } = input.body + const { did, force } = input.body await ctx.db.transaction(async (dbTxn) => { const storage = new SqlRepoStorage(dbTxn, did) - await storage.lockRepo() + await obtainLock(storage) const prevCid = await storage.getRoot() if (!prevCid) { throw new InvalidRequestError('Could not find repo') @@ -45,13 +46,23 @@ export default function (server: Server, ctx: AppContext) { data = await data.add(dataKey, cid) } const dataCid = await data.getPointer() - if (!dataCid.equals(prev.data)) { + if (!force && !dataCid.equals(prev.data)) { throw new InvalidRequestError('Data cid did not match') } const recordCids = records.map((r) => r.cid) const diff = await DataDiff.of(data, null) const cidsToKeep = [...recordCids, ...diff.newMstBlocks.cids()] const rev = TID.nextStr(prev.rev) + if (force) { + const got = await storage.getBlocks(diff.newMstBlocks.cids()) + const toAdd = diff.newMstBlocks.getMany(got.missing) + log.info( + { missing: got.missing.length }, + 'force added missing blocks', + ) + // puts any new blocks & no-ops for already existing + await storage.putMany(toAdd.blocks, rev) + } for (const chunk of chunkArray(cidsToKeep, 500)) { const cidStrs = chunk.map((c) => c.toString()) await dbTxn.db @@ -115,3 +126,15 @@ export default function (server: Server, ctx: AppContext) { }, }) } + +const obtainLock = async (storage: SqlRepoStorage, tries = 20) => { + const obtained = await storage.lockRepo() + if (obtained) { + return + } + if (tries < 1) { + throw new InvalidRequestError('could not obtain lock') + } + await wait(50) + return obtainLock(storage, tries - 1) +} diff --git a/packages/pds/src/db/index.ts b/packages/pds/src/db/index.ts index 8b1880df722..f6a1cc831a3 100644 --- a/packages/pds/src/db/index.ts +++ b/packages/pds/src/db/index.ts @@ -193,8 +193,17 @@ export class Database { return txRes } - async txAdvisoryLock(name: string): Promise { + async takeTxAdvisoryLock(name: string): Promise { this.assertTransaction() + return this.txAdvisoryLock(name) + } + + async checkTxAdvisoryLock(name: string): Promise { + this.assertNotTransaction() + return this.txAdvisoryLock(name) + } + + private async txAdvisoryLock(name: string): Promise { assert(this.dialect === 'pg', 'Postgres required') // any lock id < 10k is reserved for session locks const id = await randomIntFromSeed(name, Number.MAX_SAFE_INTEGER, 10000) diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 6628ead1154..f1077fccdf3 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -3536,6 +3536,9 @@ export const schemaDict = { type: 'string', format: 'did', }, + force: { + type: 'boolean', + }, }, }, }, diff --git a/packages/pds/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts b/packages/pds/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts index 13e68eb5c7d..c5b77876fec 100644 --- a/packages/pds/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts +++ b/packages/pds/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts @@ -12,6 +12,7 @@ export interface QueryParams {} export interface InputSchema { did: string + force?: boolean [k: string]: unknown } diff --git a/packages/pds/src/services/repo/index.ts b/packages/pds/src/services/repo/index.ts index 449842c9983..759d1c50c37 100644 --- a/packages/pds/src/services/repo/index.ts +++ b/packages/pds/src/services/repo/index.ts @@ -109,8 +109,8 @@ export class RepoService { ) { this.db.assertTransaction() const storage = new SqlRepoStorage(this.db, did, now) - const locked = await storage.lockRepo() - if (!locked) { + const obtained = await storage.lockRepo() + if (!obtained) { throw new ConcurrentWriteError() } await Promise.all([ @@ -135,8 +135,13 @@ export class RepoService { const { did, writes, swapCommitCid } = toWrite // we may have some useful cached blocks in the storage, so re-use the previous instance const storage = prevStorage ?? new SqlRepoStorage(this.db, did) - const commit = await this.formatCommit(storage, did, writes, swapCommitCid) try { + const commit = await this.formatCommit( + storage, + did, + writes, + swapCommitCid, + ) await this.serviceTx(async (srvcTx) => srvcTx.processCommit(did, writes, commit, new Date().toISOString()), ) @@ -159,6 +164,12 @@ export class RepoService { writes: PreparedWrite[], swapCommit?: CID, ): Promise { + // this is not in a txn, so this won't actually hold the lock, + // we just check if it is currently held by another txn + const available = await storage.lockAvailable() + if (!available) { + throw new ConcurrentWriteError() + } const currRoot = await storage.getRootDetailed() if (!currRoot) { throw new InvalidRequestError( @@ -171,9 +182,13 @@ export class RepoService { // cache last commit since there's likely overlap await storage.cacheRev(currRoot.rev) const recordTxn = this.services.record(this.db) + const newRecordCids: CID[] = [] const delAndUpdateUris: AtUri[] = [] for (const write of writes) { const { action, uri, swapCid } = write + if (action !== WriteOpAction.Delete) { + newRecordCids.push(write.cid) + } if (action !== WriteOpAction.Create) { delAndUpdateUris.push(uri) } @@ -195,9 +210,22 @@ export class RepoService { throw new BadRecordSwapError(currRecord) } } - const writeOps = writes.map(writeToOp) - const repo = await Repo.load(storage, currRoot.cid) - const commit = await repo.formatCommit(writeOps, this.repoSigningKey) + + let commit: CommitData + try { + const repo = await Repo.load(storage, currRoot.cid) + const writeOps = writes.map(writeToOp) + commit = await repo.formatCommit(writeOps, this.repoSigningKey) + } catch (err) { + // if an error occurs, check if it is attributable to a concurrent write + const curr = await storage.getRoot() + if (!currRoot.cid.equals(curr)) { + throw new ConcurrentWriteError() + } else { + throw err + } + } + // find blocks that would be deleted but are referenced by another record const dupeRecordCids = await this.getDuplicateRecordCids( did, @@ -207,6 +235,14 @@ export class RepoService { for (const cid of dupeRecordCids) { commit.removedCids.delete(cid) } + + // find blocks that are relevant to ops but not included in diff + // (for instance a record that was moved but cid stayed the same) + const newRecordBlocks = commit.newBlocks.getMany(newRecordCids) + if (newRecordBlocks.missing.length > 0) { + const missingBlocks = await storage.getBlocks(newRecordBlocks.missing) + commit.newBlocks.addMap(missingBlocks.blocks) + } return commit } diff --git a/packages/pds/src/sql-repo-storage.ts b/packages/pds/src/sql-repo-storage.ts index af3cf719822..24038e27790 100644 --- a/packages/pds/src/sql-repo-storage.ts +++ b/packages/pds/src/sql-repo-storage.ts @@ -24,10 +24,14 @@ export class SqlRepoStorage extends ReadableBlockstore implements RepoStorage { super() } - // note this method will return null if the repo has a lock on it currently async lockRepo(): Promise { if (this.db.dialect === 'sqlite') return true - return this.db.txAdvisoryLock(this.did) + return this.db.takeTxAdvisoryLock(this.did) + } + + async lockAvailable(): Promise { + if (this.db.dialect === 'sqlite') return true + return this.db.checkTxAdvisoryLock(this.did) } async getRoot(): Promise { @@ -201,6 +205,44 @@ export class SqlRepoStorage extends ReadableBlockstore implements RepoStorage { } } + async getCarStreamLegacy() { + const root = await this.getRoot() + if (!root) { + throw new RepoRootNotFoundError() + } + return writeCarStream(root, async (car) => { + let cursor: CID | undefined = undefined + do { + const res = await this.getBlockRangeLegacy(cursor) + for (const row of res) { + await car.put({ + cid: CID.parse(row.cid), + bytes: row.content, + }) + } + const lastRow = res.at(-1) + if (lastRow) { + cursor = CID.parse(lastRow.cid) + } else { + cursor = undefined + } + } while (cursor) + }) + } + + async getBlockRangeLegacy(cursor?: CID) { + let builder = this.db.db + .selectFrom('ipld_block') + .where('creator', '=', this.did) + .select(['cid', 'content']) + .orderBy('cid', 'asc') + .limit(500) + if (cursor) { + builder = builder.where('cid', '>', cursor.toString()) + } + return builder.execute() + } + async getCarStream(since?: string) { const root = await this.getRoot() if (!root) { diff --git a/packages/pds/tests/db.test.ts b/packages/pds/tests/db.test.ts index 2f583b0073f..6e4192cfac8 100644 --- a/packages/pds/tests/db.test.ts +++ b/packages/pds/tests/db.test.ts @@ -175,7 +175,7 @@ describe('db', () => { if (db.dialect !== 'pg') return for (let i = 0; i < 100; i++) { await db.transaction(async (dbTxn) => { - const locked = await dbTxn.txAdvisoryLock('asfd') + const locked = await dbTxn.takeTxAdvisoryLock('asfd') expect(locked).toBe(true) }) } @@ -185,18 +185,18 @@ describe('db', () => { if (db.dialect !== 'pg') return const deferable = createDeferrable() const tx1 = db.transaction(async (dbTxn) => { - const locked = await dbTxn.txAdvisoryLock('asdf') + const locked = await dbTxn.takeTxAdvisoryLock('asdf') expect(locked).toBe(true) await deferable.complete }) // give it just a second to ensure it gets the lock await wait(10) const tx2 = db.transaction(async (dbTxn) => { - const locked = await dbTxn.txAdvisoryLock('asdf') + const locked = await dbTxn.takeTxAdvisoryLock('asdf') expect(locked).toBe(false) deferable.resolve() await tx1 - const locked2 = await dbTxn.txAdvisoryLock('asdf') + const locked2 = await dbTxn.takeTxAdvisoryLock('asdf') expect(locked2).toBe(true) }) await tx2 From 3b6a8254e85cb5eb2a6d03af74480d2f5afc08a2 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 31 Aug 2023 11:47:25 -0500 Subject: [PATCH 209/237] TS config: no composite (#1528) no composite builds --- tsconfig.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index c45350986b8..e9a88ea1164 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,8 +19,7 @@ "lib": ["dom", "dom.iterable", "esnext", "webworker"], "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "isolatedModules": true, - "composite": true + "isolatedModules": true }, "exclude": ["node_modules", "**/*/dist"], "references": [ From 9a0fd91f5c3873c9b16b70be1fa854829c90d7dd Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Thu, 31 Aug 2023 15:35:47 -0500 Subject: [PATCH 210/237] Limit rev cache (#1543) * disable rev cache * disable rev cache * use limit instead of disabling * dont build branch --- packages/pds/src/sql-repo-storage.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/pds/src/sql-repo-storage.ts b/packages/pds/src/sql-repo-storage.ts index 24038e27790..c8fdf1297d5 100644 --- a/packages/pds/src/sql-repo-storage.ts +++ b/packages/pds/src/sql-repo-storage.ts @@ -64,6 +64,7 @@ export class SqlRepoStorage extends ReadableBlockstore implements RepoStorage { .where('creator', '=', this.did) .where('repoRev', '=', rev) .select(['ipld_block.cid', 'ipld_block.content']) + .limit(15) .execute() for (const row of res) { this.cache.set(CID.parse(row.cid), row.content) From f84027fe8e9fb81eccd7db8a0060c25482af81b7 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 31 Aug 2023 17:19:34 -0500 Subject: [PATCH 211/237] add getFeedSuggestions (#1542) * add getFeedSuggestions lex * move to feed namespace * codegen * add table and migration * add endpoints * remove comment * add test * fix order * rename * just remove proxy check --- lexicons/app/bsky/feed/getSuggestedFeeds.json | 31 +++++ packages/api/src/client/index.ts | 13 ++ packages/api/src/client/lexicons.ts | 44 ++++++ .../types/app/bsky/feed/getSuggestedFeeds.ts | 38 ++++++ .../api/app/bsky/feed/getSuggestedFeeds.ts | 39 ++++++ packages/bsky/src/api/index.ts | 2 + packages/bsky/src/db/database-schema.ts | 4 +- .../20230830T205507322Z-suggested-feeds.ts | 13 ++ packages/bsky/src/db/migrations/index.ts | 1 + packages/bsky/src/db/tables/suggested-feed.ts | 10 ++ packages/bsky/src/lexicon/index.ts | 12 ++ packages/bsky/src/lexicon/lexicons.ts | 44 ++++++ .../types/app/bsky/feed/getSuggestedFeeds.ts | 48 +++++++ .../feed-generation.test.ts.snap | 129 ++++++++++++++++++ packages/bsky/tests/feed-generation.test.ts | 23 ++++ .../api/app/bsky/feed/getSuggestedFeeds.ts | 19 +++ .../pds/src/app-view/api/app/bsky/index.ts | 2 + packages/pds/src/lexicon/index.ts | 12 ++ packages/pds/src/lexicon/lexicons.ts | 44 ++++++ .../types/app/bsky/feed/getSuggestedFeeds.ts | 48 +++++++ 20 files changed, 575 insertions(+), 1 deletion(-) create mode 100644 lexicons/app/bsky/feed/getSuggestedFeeds.json create mode 100644 packages/api/src/client/types/app/bsky/feed/getSuggestedFeeds.ts create mode 100644 packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts create mode 100644 packages/bsky/src/db/migrations/20230830T205507322Z-suggested-feeds.ts create mode 100644 packages/bsky/src/db/tables/suggested-feed.ts create mode 100644 packages/bsky/src/lexicon/types/app/bsky/feed/getSuggestedFeeds.ts create mode 100644 packages/pds/src/app-view/api/app/bsky/feed/getSuggestedFeeds.ts create mode 100644 packages/pds/src/lexicon/types/app/bsky/feed/getSuggestedFeeds.ts diff --git a/lexicons/app/bsky/feed/getSuggestedFeeds.json b/lexicons/app/bsky/feed/getSuggestedFeeds.json new file mode 100644 index 00000000000..db62ddd525f --- /dev/null +++ b/lexicons/app/bsky/feed/getSuggestedFeeds.json @@ -0,0 +1,31 @@ +{ + "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": "app.bsky.feed.defs#generatorView"} + } + } + } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index fd8d39a785c..f9ebf0ade63 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -98,6 +98,7 @@ import * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes' import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread' import * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts' import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy' +import * as AppBskyFeedGetSuggestedFeeds from './types/app/bsky/feed/getSuggestedFeeds' import * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline' import * as AppBskyFeedLike from './types/app/bsky/feed/like' import * as AppBskyFeedPost from './types/app/bsky/feed/post' @@ -219,6 +220,7 @@ export * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes' export * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread' export * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts' export * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy' +export * as AppBskyFeedGetSuggestedFeeds from './types/app/bsky/feed/getSuggestedFeeds' export * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline' export * as AppBskyFeedLike from './types/app/bsky/feed/like' export * as AppBskyFeedPost from './types/app/bsky/feed/post' @@ -1357,6 +1359,17 @@ export class FeedNS { }) } + getSuggestedFeeds( + params?: AppBskyFeedGetSuggestedFeeds.QueryParams, + opts?: AppBskyFeedGetSuggestedFeeds.CallOptions, + ): Promise { + return this._service.xrpc + .call('app.bsky.feed.getSuggestedFeeds', params, undefined, opts) + .catch((e) => { + throw AppBskyFeedGetSuggestedFeeds.toKnownErr(e) + }) + } + getTimeline( params?: AppBskyFeedGetTimeline.QueryParams, opts?: AppBskyFeedGetTimeline.CallOptions, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 6628ead1154..76283a33dd3 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -5302,6 +5302,49 @@ export const schemaDict = { }, }, }, + 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', @@ -6705,6 +6748,7 @@ export const ids = { 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', diff --git a/packages/api/src/client/types/app/bsky/feed/getSuggestedFeeds.ts b/packages/api/src/client/types/app/bsky/feed/getSuggestedFeeds.ts new file mode 100644 index 00000000000..ef4fe1cfefe --- /dev/null +++ b/packages/api/src/client/types/app/bsky/feed/getSuggestedFeeds.ts @@ -0,0 +1,38 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { Headers, XRPCError } from '@atproto/xrpc' +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' +import * as AppBskyFeedDefs from './defs' + +export interface QueryParams { + limit?: number + cursor?: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + cursor?: string + feeds: AppBskyFeedDefs.GeneratorView[] + [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/feed/getSuggestedFeeds.ts b/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts new file mode 100644 index 00000000000..23bf349f53e --- /dev/null +++ b/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts @@ -0,0 +1,39 @@ +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.feed.getSuggestedFeeds({ + auth: ctx.authOptionalVerifier, + handler: async ({ auth }) => { + const viewer = auth.credentials.did + + const db = ctx.db.getReplica() + const feedService = ctx.services.feed(db) + const feedsRes = await db.db + .selectFrom('suggested_feed') + .orderBy('suggested_feed.order', 'asc') + .selectAll() + .execute() + + const genInfos = await feedService.getFeedGeneratorInfos( + feedsRes.map((r) => r.uri), + viewer, + ) + const genList = Object.values(genInfos) + + const creators = genList.map((gen) => gen.creator) + const profiles = await feedService.getActorInfos(creators, viewer) + + const feedViews = genList.map((gen) => + feedService.views.formatFeedGeneratorView(gen, profiles), + ) + + return { + encoding: 'application/json', + body: { + feeds: feedViews, + }, + } + }, + }) +} diff --git a/packages/bsky/src/api/index.ts b/packages/bsky/src/api/index.ts index 54cbcd6e02e..e2b1515e412 100644 --- a/packages/bsky/src/api/index.ts +++ b/packages/bsky/src/api/index.ts @@ -3,6 +3,7 @@ import AppContext from '../context' import describeFeedGenerator from './app/bsky/feed/describeFeedGenerator' import getTimeline from './app/bsky/feed/getTimeline' import getActorFeeds from './app/bsky/feed/getActorFeeds' +import getSuggestedFeeds from './app/bsky/feed/getSuggestedFeeds' import getAuthorFeed from './app/bsky/feed/getAuthorFeed' import getFeed from './app/bsky/feed/getFeed' import getFeedGenerator from './app/bsky/feed/getFeedGenerator' @@ -60,6 +61,7 @@ export default function (server: Server, ctx: AppContext) { describeFeedGenerator(server, ctx) getTimeline(server, ctx) getActorFeeds(server, ctx) + getSuggestedFeeds(server, ctx) getAuthorFeed(server, ctx) getFeed(server, ctx) getFeedGenerator(server, ctx) diff --git a/packages/bsky/src/db/database-schema.ts b/packages/bsky/src/db/database-schema.ts index 9c6d0beaa71..4c896ef5375 100644 --- a/packages/bsky/src/db/database-schema.ts +++ b/packages/bsky/src/db/database-schema.ts @@ -28,6 +28,7 @@ import * as label from './tables/label' 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' export type DatabaseSchemaType = duplicateRecord.PartialDB & profile.PartialDB & @@ -57,7 +58,8 @@ export type DatabaseSchemaType = duplicateRecord.PartialDB & label.PartialDB & algo.PartialDB & viewParam.PartialDB & - suggestedFollow.PartialDB + suggestedFollow.PartialDB & + suggestedFeed.PartialDB export type DatabaseSchema = Kysely diff --git a/packages/bsky/src/db/migrations/20230830T205507322Z-suggested-feeds.ts b/packages/bsky/src/db/migrations/20230830T205507322Z-suggested-feeds.ts new file mode 100644 index 00000000000..fd99206be25 --- /dev/null +++ b/packages/bsky/src/db/migrations/20230830T205507322Z-suggested-feeds.ts @@ -0,0 +1,13 @@ +import { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('suggested_feed') + .addColumn('uri', 'varchar', (col) => col.primaryKey()) + .addColumn('order', 'integer', (col) => col.notNull()) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('suggested_feed').execute() +} diff --git a/packages/bsky/src/db/migrations/index.ts b/packages/bsky/src/db/migrations/index.ts index 5bb0497582b..61463fd5c4e 100644 --- a/packages/bsky/src/db/migrations/index.ts +++ b/packages/bsky/src/db/migrations/index.ts @@ -25,3 +25,4 @@ export * as _20230807T035309811Z from './20230807T035309811Z-feed-item-delete-in export * as _20230808T172902639Z from './20230808T172902639Z-repo-rev' export * as _20230810T203349843Z from './20230810T203349843Z-action-duration' export * as _20230817T195936007Z from './20230817T195936007Z-native-notifications' +export * as _20230830T205507322Z from './20230830T205507322Z-suggested-feeds' diff --git a/packages/bsky/src/db/tables/suggested-feed.ts b/packages/bsky/src/db/tables/suggested-feed.ts new file mode 100644 index 00000000000..f579229793b --- /dev/null +++ b/packages/bsky/src/db/tables/suggested-feed.ts @@ -0,0 +1,10 @@ +export const tableName = 'suggested_feed' + +export interface SuggestedFeed { + uri: string + order: number +} + +export type PartialDB = { + [tableName]: SuggestedFeed +} diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 214c5484dc7..a99d4d6e51b 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -87,6 +87,7 @@ import * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes' import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread' import * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts' import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy' +import * as AppBskyFeedGetSuggestedFeeds from './types/app/bsky/feed/getSuggestedFeeds' import * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline' import * as AppBskyGraphGetBlocks from './types/app/bsky/graph/getBlocks' import * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers' @@ -1153,6 +1154,17 @@ export class FeedNS { 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, diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 6628ead1154..76283a33dd3 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -5302,6 +5302,49 @@ export const schemaDict = { }, }, }, + 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', @@ -6705,6 +6748,7 @@ export const ids = { 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', diff --git a/packages/bsky/src/lexicon/types/app/bsky/feed/getSuggestedFeeds.ts b/packages/bsky/src/lexicon/types/app/bsky/feed/getSuggestedFeeds.ts new file mode 100644 index 00000000000..9b271335466 --- /dev/null +++ b/packages/bsky/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/bsky/tests/__snapshots__/feed-generation.test.ts.snap b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap index f420c6950fe..3ddf6266fbd 100644 --- a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap +++ b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap @@ -1270,3 +1270,132 @@ Object { ], } `; + +exports[`feed generation getSuggestedFeeds returns list of suggested feed generators 1`] = ` +Object { + "feeds": Array [ + Object { + "cid": "cids(0)", + "creator": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", + "did": "user(1)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(2)", + "following": "record(1)", + "muted": false, + }, + }, + "description": "Provides all feed candidates", + "did": "user(0)", + "displayName": "All", + "indexedAt": "1970-01-01T00:00:00.000Z", + "likeCount": 2, + "uri": "record(0)", + "viewer": Object { + "like": "record(4)", + }, + }, + Object { + "cid": "cids(3)", + "creator": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", + "did": "user(1)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(2)", + "following": "record(1)", + "muted": false, + }, + }, + "description": "Provides even-indexed feed candidates", + "did": "user(0)", + "displayName": "Even", + "indexedAt": "1970-01-01T00:00:00.000Z", + "likeCount": 0, + "uri": "record(5)", + "viewer": Object {}, + }, + Object { + "cid": "cids(4)", + "creator": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", + "did": "user(1)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(2)", + "following": "record(1)", + "muted": false, + }, + }, + "description": "Provides all feed candidates, blindly ignoring pagination limit", + "did": "user(0)", + "displayName": "Bad Pagination", + "indexedAt": "1970-01-01T00:00:00.000Z", + "likeCount": 0, + "uri": "record(6)", + "viewer": Object {}, + }, + ], +} +`; diff --git a/packages/bsky/tests/feed-generation.test.ts b/packages/bsky/tests/feed-generation.test.ts index 70eb7fe82dd..17df03de966 100644 --- a/packages/bsky/tests/feed-generation.test.ts +++ b/packages/bsky/tests/feed-generation.test.ts @@ -56,6 +56,18 @@ describe('feed generation', () => { [feedUriBadPagination.toString()]: feedGenHandler('bad-pagination'), [primeUri.toString()]: feedGenHandler('prime'), }) + + const feedSuggestions = [ + { uri: allUri.toString(), order: 1 }, + { uri: evenUri.toString(), order: 2 }, + { uri: feedUriBadPagination.toString(), order: 3 }, + { uri: primeUri.toString(), order: 4 }, + ] + await network.bsky.ctx.db + .getPrimary() + .db.insertInto('suggested_feed') + .values(feedSuggestions) + .execute() }) afterAll(async () => { @@ -316,6 +328,17 @@ describe('feed generation', () => { }) }) + describe('getSuggestedFeeds', () => { + it('returns list of suggested feed generators', async () => { + const resEven = await agent.api.app.bsky.feed.getSuggestedFeeds( + {}, + { headers: await network.serviceHeaders(sc.dids.bob) }, + ) + expect(forSnapshot(resEven.data)).toMatchSnapshot() + expect(resEven.data.feeds.map((fg) => fg.uri)).not.toContain(feedUriPrime) // taken-down + }) + }) + describe('getPopularFeedGenerators', () => { it('gets popular feed generators', async () => { const resEven = diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getSuggestedFeeds.ts b/packages/pds/src/app-view/api/app/bsky/feed/getSuggestedFeeds.ts new file mode 100644 index 00000000000..ba5fb19767b --- /dev/null +++ b/packages/pds/src/app-view/api/app/bsky/feed/getSuggestedFeeds.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../../lexicon' +import AppContext from '../../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.feed.getSuggestedFeeds({ + auth: ctx.accessVerifier, + handler: async ({ req, auth, params }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.feed.getSuggestedFeeds( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/app-view/api/app/bsky/index.ts b/packages/pds/src/app-view/api/app/bsky/index.ts index d9358460c1e..fc8c6baeace 100644 --- a/packages/pds/src/app-view/api/app/bsky/index.ts +++ b/packages/pds/src/app-view/api/app/bsky/index.ts @@ -2,6 +2,7 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import getTimeline from './feed/getTimeline' import getActorFeeds from './feed/getActorFeeds' +import getSuggestedFeeds from './feed/getSuggestedFeeds' import getAuthorFeed from './feed/getAuthorFeed' import getFeedGenerator from './feed/getFeedGenerator' import getFeedGenerators from './feed/getFeedGenerators' @@ -37,6 +38,7 @@ import unspecced from './unspecced' export default function (server: Server, ctx: AppContext) { getTimeline(server, ctx) getActorFeeds(server, ctx) + getSuggestedFeeds(server, ctx) getAuthorFeed(server, ctx) getFeedGenerator(server, ctx) getFeedGenerators(server, ctx) diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 214c5484dc7..a99d4d6e51b 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -87,6 +87,7 @@ import * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes' import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread' import * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts' import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy' +import * as AppBskyFeedGetSuggestedFeeds from './types/app/bsky/feed/getSuggestedFeeds' import * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline' import * as AppBskyGraphGetBlocks from './types/app/bsky/graph/getBlocks' import * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers' @@ -1153,6 +1154,17 @@ export class FeedNS { 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, diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index f1077fccdf3..90b0c4eb475 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -5305,6 +5305,49 @@ export const schemaDict = { }, }, }, + 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', @@ -6708,6 +6751,7 @@ export const ids = { 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', diff --git a/packages/pds/src/lexicon/types/app/bsky/feed/getSuggestedFeeds.ts b/packages/pds/src/lexicon/types/app/bsky/feed/getSuggestedFeeds.ts new file mode 100644 index 00000000000..9b271335466 --- /dev/null +++ b/packages/pds/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 From 16662b220daefc90de7751158a999d7e67687ec9 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 1 Sep 2023 10:43:30 -0500 Subject: [PATCH 212/237] @atproto/api@0.6.9 (#1546) * @atproto/api@0.6.9 * @atproto/api@0.6.10 --- packages/api/package.json | 2 +- packages/api/src/client/lexicons.ts | 3 +++ .../src/client/types/com/atproto/temp/upgradeRepoVersion.ts | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 311950384f4..3df0ef35cd3 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.8", + "version": "0.6.10", "main": "src/index.ts", "scripts": { "codegen": "yarn docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 76283a33dd3..90b0c4eb475 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -3536,6 +3536,9 @@ export const schemaDict = { type: 'string', format: 'did', }, + force: { + type: 'boolean', + }, }, }, }, diff --git a/packages/api/src/client/types/com/atproto/temp/upgradeRepoVersion.ts b/packages/api/src/client/types/com/atproto/temp/upgradeRepoVersion.ts index b8b5aa511b8..abaf3a9f1b0 100644 --- a/packages/api/src/client/types/com/atproto/temp/upgradeRepoVersion.ts +++ b/packages/api/src/client/types/com/atproto/temp/upgradeRepoVersion.ts @@ -11,6 +11,7 @@ export interface QueryParams {} export interface InputSchema { did: string + force?: boolean [k: string]: unknown } From 4fb32e1e0644335f0d61777e33f52ca43291dcc0 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 5 Sep 2023 16:49:55 +0200 Subject: [PATCH 213/237] Add missing labels that are available on the moderation tool (#1515) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add missing labels that are available on the moderation tool * Remove unused label * More cleanup * :broom: Get rid of ncii as well * :sparkles: Add new label in json instead of generated file 🤦🏽‍♂️ --- packages/api/definitions/labels.json | 6 ++++ .../api/definitions/locale/en/labels.json | 16 +++++++++- packages/api/docs/labels.md | 16 ++++++++++ .../api/src/moderation/const/label-groups.ts | 1 + packages/api/src/moderation/const/labels.ts | 30 +++++++++++++++++++ 5 files changed, 68 insertions(+), 1 deletion(-) diff --git a/packages/api/definitions/labels.json b/packages/api/definitions/labels.json index d8652795464..e5ab7d2cd36 100644 --- a/packages/api/definitions/labels.json +++ b/packages/api/definitions/labels.json @@ -206,6 +206,12 @@ "preferences": ["ignore", "warn", "hide"], "flags": [], "onwarn": "alert" + }, + { + "id": "misleading", + "preferences": ["ignore", "warn", "hide"], + "flags": [], + "onwarn": "alert" } ] } diff --git a/packages/api/definitions/locale/en/labels.json b/packages/api/definitions/locale/en/labels.json index 8d0fa82df52..9a29b4b44d5 100644 --- a/packages/api/definitions/locale/en/labels.json +++ b/packages/api/definitions/locale/en/labels.json @@ -362,5 +362,19 @@ "name": "Scam Warning", "description": "The moderators believe this is fraudulent content." } + }, + "misleading": { + "settings": { + "name": "Misleading", + "description": "Accounts which share misleading information." + }, + "account": { + "name": "Misleading", + "description": "The moderators believe this account is spreading misleading information." + }, + "content": { + "name": "Misleading", + "description": "The moderators believe this account is spreading misleading information." + } } -} \ No newline at end of file +} diff --git a/packages/api/docs/labels.md b/packages/api/docs/labels.md index a47d3921ac3..9531df460c1 100644 --- a/packages/api/docs/labels.md +++ b/packages/api/docs/labels.md @@ -256,6 +256,14 @@ ✅ alert + + + misleading + misinfo + ignore, warn, hide + ✅ + + alert @@ -518,5 +526,13 @@ on an account
Scam Warning
The moderators believe this account publishes fraudulent content.

on content
Scam Warning
The moderators believe this is fraudulent content.

+ + + misleading + + general
Misleading
Accounts which share misleading information.

+ on an account
Misleading
The moderators believe this account is spreading misleading information.

+ on content
Misleading
The moderators believe this account is spreading misleading information.

+ \ No newline at end of file diff --git a/packages/api/src/moderation/const/label-groups.ts b/packages/api/src/moderation/const/label-groups.ts index d142dfa817e..108727ef7a6 100644 --- a/packages/api/src/moderation/const/label-groups.ts +++ b/packages/api/src/moderation/const/label-groups.ts @@ -130,6 +130,7 @@ export const LABEL_GROUPS: LabelGroupDefinitionMap = { LABELS['net-abuse'], LABELS['impersonation'], LABELS['scam'], + LABELS['misleading'], ], strings: { settings: { diff --git a/packages/api/src/moderation/const/labels.ts b/packages/api/src/moderation/const/labels.ts index 59f36cd842f..7e4c91777e7 100644 --- a/packages/api/src/moderation/const/labels.ts +++ b/packages/api/src/moderation/const/labels.ts @@ -795,4 +795,34 @@ export const LABELS: LabelDefinitionMap = { }, }, }, + misleading: { + id: 'misleading', + preferences: ['ignore', 'warn', 'hide'], + flags: [], + onwarn: 'alert', + groupId: 'misinfo', + configurable: true, + strings: { + settings: { + en: { + name: 'Misleading', + description: 'Accounts which share misleading information.', + }, + }, + account: { + en: { + name: 'Misleading', + description: + 'The moderators believe this account is spreading misleading information.', + }, + }, + content: { + en: { + name: 'Misleading', + description: + 'The moderators believe this account is spreading misleading information.', + }, + }, + }, + }, } From f7186f03a9110feaee33e01e6e021975d890922c Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 5 Sep 2023 16:56:19 +0200 Subject: [PATCH 214/237] Improve matching in results from `searchRepos` (#1507) * :sparkles: Better results from searchRepos when matching by handle * :white_check_mark: Adjust tests for repo search result order * :broom: Cleanup * :recycle: Refactor to re-use search query builder * :broom: Cleanup * :white_check_mark: Add test for pagination in search result --- .../src/api/com/atproto/admin/searchRepos.ts | 19 +---- packages/bsky/src/services/actor/index.ts | 74 +++++++++++-------- .../tests/views/admin/repo-search.test.ts | 71 ++++++++++++++++-- 3 files changed, 112 insertions(+), 52 deletions(-) diff --git a/packages/bsky/src/api/com/atproto/admin/searchRepos.ts b/packages/bsky/src/api/com/atproto/admin/searchRepos.ts index 73b7c31e2b6..9945b27fcb4 100644 --- a/packages/bsky/src/api/com/atproto/admin/searchRepos.ts +++ b/packages/bsky/src/api/com/atproto/admin/searchRepos.ts @@ -1,8 +1,6 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { paginate } from '../../../../db/pagination' -import { ListKeyset } from '../../../../services/actor' export default function (server: Server, ctx: AppContext) { server.com.atproto.admin.searchRepos({ @@ -10,27 +8,18 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ params }) => { const db = ctx.db.getPrimary() const moderationService = ctx.services.moderation(db) - const { term = '', limit, cursor, invitedBy } = params + const { invitedBy } = params if (invitedBy) { throw new InvalidRequestError('The invitedBy parameter is unsupported') } - const searchField = term.startsWith('did:') ? 'did' : 'handle' - - const { ref } = db.db.dynamic - const keyset = new ListKeyset(ref('indexedAt'), ref('did')) - let resultQb = ctx.services + const { results, cursor } = await ctx.services .actor(db) - .searchQb(searchField, term) - .selectAll() - resultQb = paginate(resultQb, { keyset, cursor, limit }) - - const results = await resultQb.execute() - + .getSearchResults({ ...params, includeSoftDeleted: true }) return { encoding: 'application/json', body: { - cursor: keyset.packFromResult(results), + cursor, repos: await moderationService.views.repo(results), }, } diff --git a/packages/bsky/src/services/actor/index.ts b/packages/bsky/src/services/actor/index.ts index ef27b780a66..5012f27142f 100644 --- a/packages/bsky/src/services/actor/index.ts +++ b/packages/bsky/src/services/actor/index.ts @@ -1,11 +1,12 @@ import { sql } from 'kysely' import { Database } from '../../db' -import { DbRef, notSoftDeletedClause } from '../../db/util' +import { notSoftDeletedClause } from '../../db/util' import { ActorViews } from './views' import { ImageUriBuilder } from '../../image/uri' import { Actor } from '../../db/tables/actor' -import { TimeCidKeyset } from '../../db/pagination' import { LabelCache } from '../../label-cache' +import { TimeCidKeyset, paginate } from '../../db/pagination' +import { SearchKeyset, getUserSearchQuery } from '../util/search' export class ActorService { constructor( @@ -77,33 +78,53 @@ export class ActorService { }) } - searchQb(searchField: 'did' | 'handle' = 'handle', term?: string) { + async getSearchResults({ + cursor, + limit = 25, + term = '', + includeSoftDeleted, + }: { + cursor?: string + limit?: number + term?: string + includeSoftDeleted?: boolean + }) { + const searchField = term.startsWith('did:') ? 'did' : 'handle' + let paginatedBuilder const { ref } = this.db.db.dynamic - let builder = this.db.db.selectFrom('actor') - - // When searchField === 'did', the term will always be a valid string because - // searchField is set to 'did' after checking that the term is a valid did - if (searchField === 'did' && term) { - return builder.where('actor.did', '=', term) + const paginationOptions = { + limit, + cursor, + direction: 'asc' as const, } + let keyset - if (term) { - builder = builder.where((qb) => { - // Performing matching by word using "strict word similarity" operator. - // The more characters the user gives us, the more we can ratchet down - // the distance threshold for matching. - const threshold = term.length < 3 ? 0.9 : 0.8 - return qb - .where(distance(term, ref('handle')), '<', threshold) - .orWhereExists((q) => - q - .selectFrom('profile') - .whereRef('profile.creator', '=', 'actor.did') - .where(distance(term, ref('displayName')), '<', threshold), - ) + if (term && searchField === 'handle') { + keyset = new SearchKeyset(sql``, sql``) + paginatedBuilder = getUserSearchQuery(this.db, { + term, + includeSoftDeleted, + ...paginationOptions, + }).select('distance') + } else { + paginatedBuilder = this.db.db + .selectFrom('actor') + .select([sql`0`.as('distance')]) + keyset = new ListKeyset(ref('indexedAt'), ref('did')) + + // When searchField === 'did', the term will always be a valid string because + // searchField is set to 'did' after checking that the term is a valid did + if (term && searchField === 'did') { + paginatedBuilder = paginatedBuilder.where('actor.did', '=', term) + } + paginatedBuilder = paginate(paginatedBuilder, { + keyset, + ...paginationOptions, }) } - return builder + + const results: Actor[] = await paginatedBuilder.selectAll('actor').execute() + return { results, cursor: keyset.packFromResult(results) } } async getRepoRev(did: string | null): Promise { @@ -118,11 +139,6 @@ export class ActorService { } type ActorResult = Actor - -// Uses pg_trgm strict word similarity to check similarity between a search term and a stored value -const distance = (term: string, ref: DbRef) => - sql`(${term} <<<-> ${ref})` - export class ListKeyset extends TimeCidKeyset<{ indexedAt: string did: string // handles are treated identically to cids in TimeCidKeyset diff --git a/packages/bsky/tests/views/admin/repo-search.test.ts b/packages/bsky/tests/views/admin/repo-search.test.ts index 1e965320279..ec53418eb46 100644 --- a/packages/bsky/tests/views/admin/repo-search.test.ts +++ b/packages/bsky/tests/views/admin/repo-search.test.ts @@ -1,4 +1,4 @@ -import AtpAgent from '@atproto/api' +import AtpAgent, { ComAtprotoAdminSearchRepos } from '@atproto/api' import { wait } from '@atproto/common' import { TestNetwork } from '@atproto/dev-env' import { SeedClient } from '../../seeds/client' @@ -9,6 +9,20 @@ describe('pds admin repo search views', () => { 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({ @@ -48,23 +62,64 @@ describe('pds admin repo search views', () => { }) it('gives relevant results when searched by handle', async () => { + const term = 'car' const result = await agent.api.com.atproto.admin.searchRepos( - { term: 'car' }, + { term }, { headers }, ) const shouldContain = [ - 'cara-wiegand69.test', // Present despite repo takedown - 'eudora-dietrich4.test', // Carol Littel - 'shane-torphy52.test', //Sadie Carter - 'aliya-hodkiewicz.test', // Carlton Abernathy IV + // 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 handles = result.data.repos.map((u) => u.handle) - + 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 () => { From 90e8325e5f5ea38d0621ff40e7870275652bdacc Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 5 Sep 2023 18:45:49 -0500 Subject: [PATCH 215/237] pnpm setup (#1532) * use pnpm * fix dependency issues, replace yarn and lerna scripts * remove the main/dist scripts * update Dockerfiles * use pnpm * fix dependency issues, replace yarn and lerna scripts * remove the main/dist scripts * update Dockerfiles * update bin script * remove unused zod dep * fix type errors in pds * add types prop to packages * remove unused, bump lock * fix test running * build before test * fix pino types * format * pds depends on dev-env in test * refer to src instead of built packages * pds relies on bsky in test too * remove yarn.lock * add -r flag to root test * test push to aws * remove docker test * add publishConfig to new package * move services to top level dir (cherry picked from commit f5012bec33435a4473e9960066807623334f3aff) * update workflow paths (cherry picked from commit 5c70f0176d381ca35d6be10cfa173e22373a5b5d) * add main-to-dist script * use script in all packages, remove old Dockerfiles * remove old bsky service * remove newline * test container builds * Revert "test container builds" This reverts commit c228611f5e8e1624d4b124be4976c49590130f43. * remove unused config * test build containers * pnpm in syntax * bump dd-trace * shamefully hoist * even more shame * hoist, externalize deps * clean install for prod and smaller containers * dont build branches --------- Co-authored-by: dholms --- .../workflows/build-and-push-bsky-aws.yaml | 2 +- .../workflows/build-and-push-bsky-ghcr.yaml | 2 +- .github/workflows/build-and-push-pds-aws.yaml | 2 +- .../workflows/build-and-push-pds-ghcr.yaml | 2 +- .github/workflows/repo.yaml | 27 +- .npmrc | 1 + Makefile | 28 +- lerna.json | 6 - package.json | 13 +- packages/README.md | 4 +- packages/api/build.js | 8 - packages/api/package.json | 23 +- packages/api/update-pkg.js | 14 - packages/aws/build.js | 8 - packages/aws/package.json | 16 +- packages/bsky/build.js | 8 - packages/bsky/package.json | 50 +- packages/bsky/service/package.json | 7 - packages/bsky/service/yarn.lock | 320 - packages/bsky/src/image/logger.ts | 3 +- packages/bsky/src/indexer/logger.ts | 5 +- packages/bsky/src/ingester/logger.ts | 5 +- packages/bsky/src/logger.ts | 12 +- packages/bsky/update-pkg.js | 14 - packages/common-web/build.js | 8 - packages/common-web/package.json | 15 +- packages/common-web/update-pkg.js | 14 - packages/common/build.js | 8 - packages/common/package.json | 20 +- packages/common/src/fs.ts | 3 +- packages/common/update-pkg.js | 14 - packages/crypto/build.js | 8 - packages/crypto/package.json | 13 +- packages/crypto/update-pkg.js | 14 - packages/dev-env/build.js | 10 +- packages/dev-env/package.json | 29 +- packages/dev-env/update-pkg.js | 14 - packages/identifier/build.js | 8 - packages/identifier/package.json | 15 +- packages/identifier/update-pkg.js | 14 - packages/identity/build.js | 8 - packages/identity/package.json | 17 +- packages/identity/update-pkg.js | 14 - packages/lex-cli/build.js | 8 - packages/lex-cli/package.json | 18 +- packages/lexicon/build.js | 8 - packages/lexicon/package.json | 19 +- packages/lexicon/update-pkg.js | 14 - packages/nsid/build.js | 8 - packages/nsid/package.json | 13 +- packages/pds/build.js | 8 - packages/pds/package.json | 49 +- packages/pds/service/package.json | 7 - packages/pds/service/yarn.lock | 320 - packages/pds/update-pkg.js | 14 - packages/repo/build.js | 8 - packages/repo/package.json | 26 +- packages/repo/src/logger.ts | 3 +- packages/repo/src/types.ts | 3 +- packages/repo/update-pkg.js | 14 - packages/syntax/package.json | 15 +- packages/syntax/update-pkg.js | 14 - packages/uri/build.js | 8 - packages/uri/package.json | 15 +- packages/uri/update-pkg.js | 14 - packages/xrpc-server/build.js | 8 - packages/xrpc-server/package.json | 26 +- packages/xrpc-server/src/logger.ts | 3 +- packages/xrpc-server/src/stream/logger.ts | 3 +- packages/xrpc-server/update-pkg.js | 14 - packages/xrpc/build.js | 8 - packages/xrpc/package.json | 15 +- packages/xrpc/update-pkg.js | 14 - pnpm-lock.yaml | 10611 ++++++++++++++ pnpm-workspace.yaml | 3 + {packages => services}/bsky/Dockerfile | 24 +- .../bsky/service => services/bsky}/api.js | 0 .../bsky/service => services/bsky}/indexer.js | 0 .../service => services/bsky}/ingester.js | 0 services/bsky/package.json | 9 + {packages => services}/pds/Dockerfile | 25 +- .../pds/service => services/pds}/index.js | 0 services/pds/package.json | 10 + update-main-to-dist.js | 11 + yarn.lock | 12205 ---------------- 85 files changed, 10971 insertions(+), 13450 deletions(-) create mode 100644 .npmrc delete mode 100644 lerna.json delete mode 100644 packages/api/update-pkg.js delete mode 100644 packages/bsky/service/package.json delete mode 100644 packages/bsky/service/yarn.lock delete mode 100644 packages/bsky/update-pkg.js delete mode 100644 packages/common-web/update-pkg.js delete mode 100644 packages/common/update-pkg.js delete mode 100644 packages/crypto/update-pkg.js delete mode 100644 packages/dev-env/update-pkg.js delete mode 100644 packages/identifier/update-pkg.js delete mode 100644 packages/identity/update-pkg.js delete mode 100644 packages/lexicon/update-pkg.js delete mode 100644 packages/pds/service/package.json delete mode 100644 packages/pds/service/yarn.lock delete mode 100644 packages/pds/update-pkg.js delete mode 100644 packages/repo/update-pkg.js delete mode 100644 packages/syntax/update-pkg.js delete mode 100644 packages/uri/update-pkg.js delete mode 100644 packages/xrpc-server/update-pkg.js delete mode 100644 packages/xrpc/update-pkg.js create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml rename {packages => services}/bsky/Dockerfile (70%) rename {packages/bsky/service => services/bsky}/api.js (100%) rename {packages/bsky/service => services/bsky}/indexer.js (100%) rename {packages/bsky/service => services/bsky}/ingester.js (100%) create mode 100644 services/bsky/package.json rename {packages => services}/pds/Dockerfile (72%) rename {packages/pds/service => services/pds}/index.js (100%) create mode 100644 services/pds/package.json create mode 100644 update-main-to-dist.js delete mode 100644 yarn.lock diff --git a/.github/workflows/build-and-push-bsky-aws.yaml b/.github/workflows/build-and-push-bsky-aws.yaml index 009b2bff01b..36b1aa23cb3 100644 --- a/.github/workflows/build-and-push-bsky-aws.yaml +++ b/.github/workflows/build-and-push-bsky-aws.yaml @@ -47,7 +47,7 @@ jobs: with: context: . push: ${{ github.event_name != 'pull_request' }} - file: ./packages/bsky/Dockerfile + file: ./services/bsky/Dockerfile tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/.github/workflows/build-and-push-bsky-ghcr.yaml b/.github/workflows/build-and-push-bsky-ghcr.yaml index f31284be315..5d22cd9a389 100644 --- a/.github/workflows/build-and-push-bsky-ghcr.yaml +++ b/.github/workflows/build-and-push-bsky-ghcr.yaml @@ -49,7 +49,7 @@ jobs: with: context: . push: ${{ github.event_name != 'pull_request' }} - file: ./packages/bsky/Dockerfile + file: ./services/bsky/Dockerfile tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/.github/workflows/build-and-push-pds-aws.yaml b/.github/workflows/build-and-push-pds-aws.yaml index 8b821682463..097f782d88e 100644 --- a/.github/workflows/build-and-push-pds-aws.yaml +++ b/.github/workflows/build-and-push-pds-aws.yaml @@ -47,7 +47,7 @@ jobs: with: context: . push: ${{ github.event_name != 'pull_request' }} - file: ./packages/pds/Dockerfile + file: ./services/pds/Dockerfile tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/.github/workflows/build-and-push-pds-ghcr.yaml b/.github/workflows/build-and-push-pds-ghcr.yaml index ef4036cdeb8..b11230ab531 100644 --- a/.github/workflows/build-and-push-pds-ghcr.yaml +++ b/.github/workflows/build-and-push-pds-ghcr.yaml @@ -49,7 +49,7 @@ jobs: with: context: . push: ${{ github.event_name != 'pull_request' }} - file: ./packages/pds/Dockerfile + file: ./services/pds/Dockerfile tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/.github/workflows/repo.yaml b/.github/workflows/repo.yaml index f04ab7b6914..8f25afdcbbe 100644 --- a/.github/workflows/repo.yaml +++ b/.github/workflows/repo.yaml @@ -12,12 +12,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + with: + version: 8 - uses: actions/setup-node@v3 with: node-version: 18 - cache: "yarn" - - run: yarn install --frozen-lockfile - - run: yarn build + cache: "pnpm" + - run: pnpm i --frozen-lockfile + - run: pnpm build test: strategy: matrix: @@ -25,19 +28,25 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + with: + version: 8 - uses: actions/setup-node@v3 with: node-version: 18 - cache: "yarn" - - run: yarn install --frozen-lockfile - - run: yarn test:withFlags --maxWorkers=1 --shard=${{ matrix.shard }} --passWithNoTests + cache: "pnpm" + - run: pnpm i --frozen-lockfile + - run: pnpm test:withFlags --maxWorkers=1 --shard=${{ matrix.shard }} --passWithNoTests verify: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + with: + version: 8 - uses: actions/setup-node@v3 with: node-version: 18 - cache: "yarn" - - run: yarn install --frozen-lockfile - - run: yarn verify + cache: "pnpm" + - run: pnpm install --frozen-lockfile + - run: pnpm verify diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000000..8e012302ad8 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +enable-pre-post-scripts = true diff --git a/Makefile b/Makefile index 138575cd3a1..1ed13a81ff3 100644 --- a/Makefile +++ b/Makefile @@ -12,46 +12,46 @@ help: ## Print info about all commands .PHONY: build build: ## Compile all modules - yarn build + pnpm build .PHONY: test test: ## Run all tests - yarn test + pnpm test .PHONY: run-dev-env run-dev-env: ## Run a "development environment" shell - cd packages/dev-env; yarn run start + cd packages/dev-env; pnpm run start .PHONY: run-dev-pds run-dev-pds: ## Run PDS locally if [ ! -f "packages/pds/.dev.env" ]; then cp packages/pds/example.dev.env packages/pds/.dev.env; fi - cd packages/pds; ENV=dev yarn run start | yarn exec pino-pretty + cd packages/pds; ENV=dev pnpm run start | pnpm exec pino-pretty .PHONY: run-dev-bsky run-dev-bsky: ## Run appview ('bsky') locally if [ ! -f "packages/bsky/.dev.env" ]; then cp packages/bsky/example.dev.env packages/bsky/.dev.env; fi - cd packages/bsky; ENV=dev yarn run start | yarn exec pino-pretty + cd packages/bsky; ENV=dev pnpm run start | pnpm exec pino-pretty .PHONY: codegen codegen: ## Re-generate packages from lexicon/ files - cd packages/api; yarn run codegen - cd packages/pds; yarn run codegen - cd packages/bsky; yarn run codegen + cd packages/api; pnpm run codegen + cd packages/pds; pnpm run codegen + cd packages/bsky; pnpm run codegen .PHONY: lint lint: ## Run style checks and verify syntax - yarn verify + pnpm verify .PHONY: fmt fmt: ## Run syntax re-formatting - yarn prettier + pnpm prettier .PHONY: deps -deps: ## Installs dependent libs using 'yarn install' - yarn install --frozen-lockfile +deps: ## Installs dependent libs using 'pnpm install' + pnpm install --frozen-lockfile .PHONY: nvm-setup -nvm-setup: ## Use NVM to install and activate node+yarn +nvm-setup: ## Use NVM to install and activate node+pnpm nvm install 18 nvm use 18 - npm install --global yarn + npm install --global pnpm diff --git a/lerna.json b/lerna.json deleted file mode 100644 index 2e245ac66a8..00000000000 --- a/lerna.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "packages": ["packages/*"], - "npmClient": "yarn", - "useWorkspaces": true, - "version": "0.0.1" -} \ No newline at end of file diff --git a/package.json b/package.json index 93475e7a9c0..c13995d6ca6 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,12 @@ "node": ">=18" }, "scripts": { - "prepublish": "yarn build", - "verify": "lerna run verify --stream", - "prettier": "lerna run prettier", - "build": "lerna run build", - "test": "LOG_ENABLED=false NODE_ENV=development ./packages/dev-infra/with-test-redis-and-db.sh lerna run test --stream", - "test:withFlags": "LOG_ENABLED=false NODE_ENV=development ./packages/dev-infra/with-test-redis-and-db.sh lerna run test --stream --" + "verify": "pnpm -r --stream lint && pnpm -r --stream prettier", + "format": "pnpm -r --stream prettier:fix", + "build": "pnpm -r --stream build", + "update-main-to-dist": "pnpm -r --stream update-main-to-dist", + "test": "LOG_ENABLED=false NODE_ENV=development ./packages/dev-infra/with-test-redis-and-db.sh pnpm --stream -r test", + "test:withFlags": "LOG_ENABLED=false NODE_ENV=development ./packages/dev-infra/with-test-redis-and-db.sh pnpm --stream -r test --" }, "devDependencies": { "@babel/core": "^7.18.6", @@ -35,7 +35,6 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", "jest": "^28.1.2", - "lerna": "^4.0.0", "node-gyp": "^9.3.1", "npm-run-all": "^4.1.5", "pino-pretty": "^9.1.0", diff --git a/packages/README.md b/packages/README.md index 3d8c577d0c6..ba77e031e8c 100644 --- a/packages/README.md +++ b/packages/README.md @@ -21,11 +21,11 @@ Only applicable to packages which contain benchmarks(`jest.bench.config.js`). -You can run benchmarks with `yarn bench`. +You can run benchmarks with `pnpm bench`. ### Attaching a profiler -Running `yarn bench:profile` will launch `bench` with `--inspect-brk` flag. +Running `pnpm bench:profile` will launch `bench` with `--inspect-brk` flag. Execution will be paused until a debugger is attached, you can read more about node debuggers [here](https://nodejs.org/en/docs/guides/debugging-getting-started#inspector-clients) diff --git a/packages/api/build.js b/packages/api/build.js index 5570ef4ee72..30fbe7cea56 100644 --- a/packages/api/build.js +++ b/packages/api/build.js @@ -1,16 +1,8 @@ -const pkgJson = require('@npmcli/package-json') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts'], diff --git a/packages/api/package.json b/packages/api/package.json index 3df0ef35cd3..dad7c689fbd 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -2,15 +2,16 @@ "name": "@atproto/api", "version": "0.6.10", "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "scripts": { - "codegen": "yarn docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", + "codegen": "pnpm docgen && node ./scripts/generate-code.mjs && lex gen-api ./src/client ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/*", "docgen": "node ./scripts/generate-docs.mjs", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", - "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", - "update-main-to-src": "node ./update-pkg.js --update-main-to-src", - "prepublish": "npm run update-main-to-dist", - "postpublish": "npm run update-main-to-src", + "update-main-to-dist": "node ../../update-main-to-dist.js packages/api", "test": "jest", "bench": "jest --config jest.bench.config.js", "bench:profile": "node --inspect-brk ../../node_modules/.bin/jest --config jest.bench.config.js" @@ -22,15 +23,17 @@ "directory": "packages/api" }, "dependencies": { - "@atproto/common-web": "*", - "@atproto/syntax": "*", - "@atproto/xrpc": "*", + "@atproto/common-web": "workspace:^", + "@atproto/lexicon": "workspace:^", + "@atproto/syntax": "workspace:^", + "@atproto/xrpc": "workspace:^", + "multiformats": "^9.9.0", "tlds": "^1.234.0", "typed-emitter": "^2.1.0" }, "devDependencies": { - "@atproto/lex-cli": "*", - "@atproto/pds": "*", + "@atproto/lex-cli": "workspace:^", + "@atproto/pds": "workspace:^", "common-tags": "^1.8.2" } } diff --git a/packages/api/update-pkg.js b/packages/api/update-pkg.js deleted file mode 100644 index 15d70bdab33..00000000000 --- a/packages/api/update-pkg.js +++ /dev/null @@ -1,14 +0,0 @@ -const pkgJson = require('@npmcli/package-json') - -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} -if (process.argv.includes('--update-main-to-src')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'src/index.ts' })) - .then((pkg) => pkg.save()) -} diff --git a/packages/aws/build.js b/packages/aws/build.js index 5628aa4f4eb..e880ae9930b 100644 --- a/packages/aws/build.js +++ b/packages/aws/build.js @@ -1,16 +1,8 @@ -const pkgJson = require('@npmcli/package-json') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts'], diff --git a/packages/aws/package.json b/packages/aws/package.json index 5a8d0eb0cd7..192149feb69 100644 --- a/packages/aws/package.json +++ b/packages/aws/package.json @@ -2,6 +2,10 @@ "name": "@atproto/aws", "version": "0.1.0", "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/src/index.d.ts" + }, "license": "MIT", "repository": { "type": "git", @@ -12,21 +16,23 @@ "prettier": "prettier --check src/", "prettier:fix": "prettier --write src/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", + "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", - "postbuild": "tsc --build tsconfig.build.json" + "postbuild": "tsc --build tsconfig.build.json", + "update-main-to-dist": "node ../../update-main-to-dist.js packages/aws" }, "dependencies": { - "@atproto/crypto": "*", + "@atproto/crypto": "workspace:^", + "@atproto/repo": "workspace:^", "@aws-sdk/client-cloudfront": "^3.261.0", "@aws-sdk/client-kms": "^3.196.0", "@aws-sdk/client-s3": "^3.224.0", "@aws-sdk/lib-storage": "^3.226.0", "@noble/curves": "^1.1.0", "key-encoder": "^2.0.3", - "multiformats": "^9.6.4", + "multiformats": "^9.9.0", "uint8arrays": "3.0.0" } } diff --git a/packages/bsky/build.js b/packages/bsky/build.js index f27a88ec0cf..3822d9bc98f 100644 --- a/packages/bsky/build.js +++ b/packages/bsky/build.js @@ -1,16 +1,8 @@ -const pkgJson = require('@npmcli/package-json') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts', 'src/db/index.ts'], diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 35079fb4238..177435387cd 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -8,11 +8,16 @@ "directory": "packages/bsky" }, "main": "src/index.ts", - "bin": "dist/bin.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/bsky", "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", @@ -20,24 +25,20 @@ "prettier": "prettier --check src/ tests/", "prettier:fix": "prettier --write src/ tests/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", - "migration:create": "ts-node ./bin/migration-create.ts", - "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", - "update-main-to-src": "node ./update-pkg.js --update-main-to-src", - "prepublish": "npm run update-main-to-dist", - "postpublish": "npm run update-main-to-src" + "verify:fix": "pnpm prettier:fix && pnpm lint:fix", + "migration:create": "ts-node ./bin/migration-create.ts" }, "dependencies": { - "@atproto/api": "*", - "@atproto/common": "*", - "@atproto/crypto": "*", - "@atproto/syntax": "*", - "@atproto/identity": "*", - "@atproto/lexicon": "*", - "@atproto/repo": "*", - "@atproto/xrpc-server": "*", + "@atproto/api": "workspace:^", + "@atproto/common": "workspace:^", + "@atproto/crypto": "workspace:^", + "@atproto/syntax": "workspace:^", + "@atproto/identity": "workspace:^", + "@atproto/lexicon": "workspace:^", + "@atproto/repo": "workspace:^", + "@atproto/xrpc-server": "workspace:^", "@did-plc/lib": "^0.0.1", "@isaacs/ttlcache": "^1.4.1", "compression": "^1.7.4", @@ -45,30 +46,33 @@ "dotenv": "^16.0.0", "express": "^4.17.2", "express-async-errors": "^3.1.1", + "form-data": "^4.0.0", "http-errors": "^2.0.0", "http-terminator": "^3.2.0", "ioredis": "^5.3.2", "iso-datestring-validator": "^2.2.2", "kysely": "^0.22.0", - "multiformats": "^9.6.4", + "multiformats": "^9.9.0", "p-queue": "^6.6.2", "pg": "^8.10.0", - "pino": "^8.6.1", + "pino": "^8.15.0", "pino-http": "^8.2.1", "sharp": "^0.31.2", "typed-emitter": "^2.1.0", "uint8arrays": "3.0.0" }, "devDependencies": { - "@atproto/api": "*", - "@atproto/dev-env": "*", - "@atproto/lex-cli": "*", - "@atproto/pds": "*", - "@atproto/xrpc": "*", + "@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", "@types/sharp": "^0.31.0", "axios": "^0.27.2" } diff --git a/packages/bsky/service/package.json b/packages/bsky/service/package.json deleted file mode 100644 index 52fa4decbd1..00000000000 --- a/packages/bsky/service/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "bsky-app-view-service", - "private": true, - "dependencies": { - "dd-trace": "^3.8.0" - } -} diff --git a/packages/bsky/service/yarn.lock b/packages/bsky/service/yarn.lock deleted file mode 100644 index c1090549f70..00000000000 --- a/packages/bsky/service/yarn.lock +++ /dev/null @@ -1,320 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@datadog/native-appsec@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-2.0.0.tgz#ad65ba19bfd68e6b6c6cf64bb8ef55d099af8edc" - integrity sha512-XHARZ6MVgbnfOUO6/F3ZoZ7poXHJCNYFlgcyS2Xetuk9ITA5bfcooX2B2F7tReVB+RLJ+j8bsm0t55SyF04KDw== - dependencies: - node-gyp-build "^3.9.0" - -"@datadog/native-iast-rewriter@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-1.1.2.tgz#793cbf92d218ec80d645be0830023656b81018ea" - integrity sha512-pigRfRtAjZjMjqIXyXb98S4aDnuHz/EmqpoxAajFZsNjBLM87YonwSY5zoBdCsOyA46ddKOJRoCQd5ZalpOFMQ== - dependencies: - node-gyp-build "^4.5.0" - -"@datadog/native-iast-taint-tracking@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-1.1.0.tgz#8f7d0016157b32dbf5c01b15b8afb1c4286b4a18" - integrity sha512-TOrngpt6Qh52zWFOz1CkFXw0g43rnuUziFBtIMUsOLGzSHr9wdnTnE6HAyuvKy3f3ecAoZESlMfilGRKP93hXQ== - dependencies: - node-gyp-build "^3.9.0" - -"@datadog/native-metrics@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@datadog/native-metrics/-/native-metrics-1.5.0.tgz#e71b6b6d65f4bd58dfdffab2737890e8eef34584" - integrity sha512-K63XMDx74RLhOpM8I9GGZR9ft0CNNB/RkjYPLHcVGvVnBR47zmWE2KFa7Yrtzjbk73+88PXI4nzqLyR3PJsaIQ== - dependencies: - node-gyp-build "^3.9.0" - -"@datadog/pprof@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-1.1.1.tgz#17e86035140523ac3a96f3662e5dd29822042d61" - integrity sha512-5lYXUpikQhrJwzODtJ7aFM0oKmPccISnTCecuWhjxIj4/7UJv0DamkLak634bgEW+kiChgkKFDapHSesuXRDXQ== - dependencies: - delay "^5.0.0" - findit2 "^2.2.3" - node-gyp-build "^3.9.0" - p-limit "^3.1.0" - pify "^5.0.0" - protobufjs "^7.0.0" - source-map "^0.7.3" - split "^1.0.1" - -"@datadog/sketches-js@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@datadog/sketches-js/-/sketches-js-2.1.0.tgz#8c7e8028a5fc22ad102fa542b0a446c956830455" - integrity sha512-smLocSfrt3s53H/XSVP3/1kP42oqvrkjUPtyaFd1F79ux24oE31BKt+q0c6lsa6hOYrFzsIwyc5GXAI5JmfOew== - -"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" - integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== - -"@protobufjs/base64@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" - integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== - -"@protobufjs/codegen@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" - integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== - -"@protobufjs/eventemitter@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" - integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== - -"@protobufjs/fetch@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" - integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== - dependencies: - "@protobufjs/aspromise" "^1.1.1" - "@protobufjs/inquire" "^1.1.0" - -"@protobufjs/float@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" - integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== - -"@protobufjs/inquire@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" - integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== - -"@protobufjs/path@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" - integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== - -"@protobufjs/pool@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" - integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== - -"@protobufjs/utf8@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" - integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== - -"@types/node@>=13.7.0": - version "18.13.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850" - integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg== - -crypto-randomuuid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/crypto-randomuuid/-/crypto-randomuuid-1.0.0.tgz#acf583e5e085e867ae23e107ff70279024f9e9e7" - integrity sha512-/RC5F4l1SCqD/jazwUF6+t34Cd8zTSAGZ7rvvZu1whZUhD2a5MOGKjSGowoGcpj/cbVZk1ZODIooJEQQq3nNAA== - -dd-trace@^3.8.0: - version "3.13.2" - resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-3.13.2.tgz#95b1ec480ab9ac406e1da7591a8c6f678d3799fd" - integrity sha512-POO9nEcAufe5pgp2xV1X3PfWip6wh+6TpEcRSlSgZJCIIMvWVCkcIVL/J2a6KAZq6V3Yjbkl8Ktfe+MOzQf5kw== - dependencies: - "@datadog/native-appsec" "2.0.0" - "@datadog/native-iast-rewriter" "1.1.2" - "@datadog/native-iast-taint-tracking" "1.1.0" - "@datadog/native-metrics" "^1.5.0" - "@datadog/pprof" "^1.1.1" - "@datadog/sketches-js" "^2.1.0" - crypto-randomuuid "^1.0.0" - diagnostics_channel "^1.1.0" - ignore "^5.2.0" - import-in-the-middle "^1.3.4" - ipaddr.js "^2.0.1" - istanbul-lib-coverage "3.2.0" - koalas "^1.0.2" - limiter "^1.1.4" - lodash.kebabcase "^4.1.1" - lodash.pick "^4.4.0" - lodash.sortby "^4.7.0" - lodash.uniq "^4.5.0" - lru-cache "^7.14.0" - methods "^1.1.2" - module-details-from-path "^1.0.3" - node-abort-controller "^3.0.1" - opentracing ">=0.12.1" - path-to-regexp "^0.1.2" - protobufjs "^7.1.2" - retry "^0.10.1" - semver "^5.5.0" - -delay@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" - integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== - -diagnostics_channel@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/diagnostics_channel/-/diagnostics_channel-1.1.0.tgz#bd66c49124ce3bac697dff57466464487f57cea5" - integrity sha512-OE1ngLDjSBPG6Tx0YATELzYzy3RKHC+7veQ8gLa8yS7AAgw65mFbVdcsu3501abqOZCEZqZyAIemB0zXlqDSuw== - -findit2@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/findit2/-/findit2-2.2.3.tgz#58a466697df8a6205cdfdbf395536b8bd777a5f6" - integrity sha512-lg/Moejf4qXovVutL0Lz4IsaPoNYMuxt4PA0nGqFxnJ1CTTGGlEO2wKgoDpwknhvZ8k4Q2F+eesgkLbG2Mxfog== - -ignore@^5.2.0: - version "5.2.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" - integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== - -import-in-the-middle@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.3.4.tgz#7074bbd4e84e8cdafd1eae400b04e6fe252a0768" - integrity sha512-TUXqqEFacJ2DWAeYOhHwGZTMJtFxFVw0C1pYA+AXmuWXZGnBqUhHdtVrSkSbW5D7k2yriBG45j23iH9TRtI+bQ== - dependencies: - module-details-from-path "^1.0.3" - -ipaddr.js@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" - integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== - -istanbul-lib-coverage@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" - integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== - -koalas@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/koalas/-/koalas-1.0.2.tgz#318433f074235db78fae5661a02a8ca53ee295cd" - integrity sha512-RYhBbYaTTTHId3l6fnMZc3eGQNW6FVCqMG6AMwA5I1Mafr6AflaXeoi6x3xQuATRotGYRLk6+1ELZH4dstFNOA== - -limiter@^1.1.4: - version "1.1.5" - resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" - integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== - -lodash.kebabcase@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" - integrity sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g== - -lodash.pick@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" - integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== - -lodash.sortby@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" - integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== - -lodash.uniq@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== - -long@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/long/-/long-5.2.1.tgz#e27595d0083d103d2fa2c20c7699f8e0c92b897f" - integrity sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A== - -lru-cache@^7.14.0: - version "7.16.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.16.0.tgz#b1b946cff368d3f3c569cc3d6a5ba8f90435160f" - integrity sha512-VJBdeMa9Bz27NNlx+DI/YXGQtXdjUU+9gdfN1rYfra7vtTjhodl5tVNmR42bo+ORHuDqDT+lGAUAb+lzvY42Bw== - -methods@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== - -module-details-from-path@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" - integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A== - -node-abort-controller@^3.0.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" - integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== - -node-gyp-build@^3.9.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.9.0.tgz#53a350187dd4d5276750da21605d1cb681d09e25" - integrity sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A== - -node-gyp-build@^4.5.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" - integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== - -opentracing@>=0.12.1: - version "0.14.7" - resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.14.7.tgz#25d472bd0296dc0b64d7b94cbc995219031428f5" - integrity sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q== - -p-limit@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -path-to-regexp@^0.1.2: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== - -pify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" - integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== - -protobufjs@^7.0.0, protobufjs@^7.1.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.2.tgz#2af401d8c547b9476fb37ffc65782cf302342ca3" - integrity sha512-++PrQIjrom+bFDPpfmqXfAGSQs40116JRrqqyf53dymUMvvb5d/LMRyicRoF1AUKoXVS1/IgJXlEgcpr4gTF3Q== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/node" ">=13.7.0" - long "^5.0.0" - -retry@^0.10.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" - integrity sha512-ZXUSQYTHdl3uS7IuCehYfMzKyIDBNoAuUblvy5oGO5UJSUTmStUUVPXbA9Qxd173Bgre53yCQczQuHgRWAdvJQ== - -semver@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -source-map@^0.7.3: - version "0.7.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== - -split@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" - integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== - dependencies: - through "2" - -through@2: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/packages/bsky/src/image/logger.ts b/packages/bsky/src/image/logger.ts index 71893bfae6c..d3d25481f81 100644 --- a/packages/bsky/src/image/logger.ts +++ b/packages/bsky/src/image/logger.ts @@ -1,5 +1,6 @@ import { subsystemLogger } from '@atproto/common' -export const logger = subsystemLogger('bsky:image') +export const logger: ReturnType = + subsystemLogger('bsky:image') export default logger diff --git a/packages/bsky/src/indexer/logger.ts b/packages/bsky/src/indexer/logger.ts index dc981563732..45752727f99 100644 --- a/packages/bsky/src/indexer/logger.ts +++ b/packages/bsky/src/indexer/logger.ts @@ -1,3 +1,6 @@ import { subsystemLogger } from '@atproto/common' -export default subsystemLogger('bsky:indexer') +const logger: ReturnType = + subsystemLogger('bsky:indexer') + +export default logger diff --git a/packages/bsky/src/ingester/logger.ts b/packages/bsky/src/ingester/logger.ts index 0351db66ca9..49855166481 100644 --- a/packages/bsky/src/ingester/logger.ts +++ b/packages/bsky/src/ingester/logger.ts @@ -1,3 +1,6 @@ import { subsystemLogger } from '@atproto/common' -export default subsystemLogger('bsky:ingester') +const logger: ReturnType = + subsystemLogger('bsky:ingester') + +export default logger diff --git a/packages/bsky/src/logger.ts b/packages/bsky/src/logger.ts index edfd6cd4d81..fb6c8b32e43 100644 --- a/packages/bsky/src/logger.ts +++ b/packages/bsky/src/logger.ts @@ -1,10 +1,14 @@ import pinoHttp from 'pino-http' import { subsystemLogger } from '@atproto/common' -export const dbLogger = subsystemLogger('bsky:db') -export const subLogger = subsystemLogger('bsky:sub') -export const labelerLogger = subsystemLogger('bsky:labeler') -export const httpLogger = subsystemLogger('bsky') +export const dbLogger: ReturnType = + subsystemLogger('bsky:db') +export const subLogger: ReturnType = + subsystemLogger('bsky:sub') +export const labelerLogger: ReturnType = + subsystemLogger('bsky:labeler') +export const httpLogger: ReturnType = + subsystemLogger('bsky') export const loggerMiddleware = pinoHttp({ logger: httpLogger, diff --git a/packages/bsky/update-pkg.js b/packages/bsky/update-pkg.js deleted file mode 100644 index 15d70bdab33..00000000000 --- a/packages/bsky/update-pkg.js +++ /dev/null @@ -1,14 +0,0 @@ -const pkgJson = require('@npmcli/package-json') - -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} -if (process.argv.includes('--update-main-to-src')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'src/index.ts' })) - .then((pkg) => pkg.save()) -} diff --git a/packages/common-web/build.js b/packages/common-web/build.js index 5570ef4ee72..30fbe7cea56 100644 --- a/packages/common-web/build.js +++ b/packages/common-web/build.js @@ -1,16 +1,8 @@ -const pkgJson = require('@npmcli/package-json') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts'], diff --git a/packages/common-web/package.json b/packages/common-web/package.json index d5737b0f7e4..fb1552e0133 100644 --- a/packages/common-web/package.json +++ b/packages/common-web/package.json @@ -2,6 +2,10 @@ "name": "@atproto/common-web", "version": "0.2.0", "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "license": "MIT", "repository": { "type": "git", @@ -13,19 +17,16 @@ "prettier": "prettier --check src/ tests/", "prettier:fix": "prettier --write src/ tests/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", + "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", - "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", - "update-main-to-src": "node ./update-pkg.js --update-main-to-src", - "prepublish": "npm run update-main-to-dist", - "postpublish": "npm run update-main-to-src" + "update-main-to-dist": "node ../../update-main-to-dist.js packages/common-web" }, "dependencies": { "graphemer": "^1.4.0", - "multiformats": "^9.6.4", + "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.21.4" } diff --git a/packages/common-web/update-pkg.js b/packages/common-web/update-pkg.js deleted file mode 100644 index 15d70bdab33..00000000000 --- a/packages/common-web/update-pkg.js +++ /dev/null @@ -1,14 +0,0 @@ -const pkgJson = require('@npmcli/package-json') - -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} -if (process.argv.includes('--update-main-to-src')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'src/index.ts' })) - .then((pkg) => pkg.save()) -} diff --git a/packages/common/build.js b/packages/common/build.js index 5628aa4f4eb..e880ae9930b 100644 --- a/packages/common/build.js +++ b/packages/common/build.js @@ -1,16 +1,8 @@ -const pkgJson = require('@npmcli/package-json') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts'], diff --git a/packages/common/package.json b/packages/common/package.json index 11d5cbf98dd..d87d7190287 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -2,6 +2,10 @@ "name": "@atproto/common", "version": "0.3.0", "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "license": "MIT", "repository": { "type": "git", @@ -13,21 +17,19 @@ "prettier": "prettier --check src/ tests/", "prettier:fix": "prettier --write src/ tests/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", + "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", - "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", - "update-main-to-src": "node ./update-pkg.js --update-main-to-src", - "prepublish": "npm run update-main-to-dist", - "postpublish": "npm run update-main-to-src" + "update-main-to-dist": "node ../../update-main-to-dist.js packages/common" }, "dependencies": { - "@atproto/common-web": "*", + "@atproto/common-web": "workspace:^", "@ipld/dag-cbor": "^7.0.3", "cbor-x": "^1.5.1", - "multiformats": "^9.6.4", - "pino": "^8.6.1" + "multiformats": "^9.9.0", + "pino": "^8.15.0", + "zod": "3.21.4" } } diff --git a/packages/common/src/fs.ts b/packages/common/src/fs.ts index a0af96a4e1c..db7c586b621 100644 --- a/packages/common/src/fs.ts +++ b/packages/common/src/fs.ts @@ -1,9 +1,10 @@ import { isErrnoException } from '@atproto/common-web' +import { constants } from 'fs' import fs from 'fs/promises' export const fileExists = async (location: string): Promise => { try { - await fs.access(location, fs.constants.F_OK) + await fs.access(location, constants.F_OK) return true } catch (err) { if (isErrnoException(err) && err.code === 'ENOENT') { diff --git a/packages/common/update-pkg.js b/packages/common/update-pkg.js deleted file mode 100644 index 15d70bdab33..00000000000 --- a/packages/common/update-pkg.js +++ /dev/null @@ -1,14 +0,0 @@ -const pkgJson = require('@npmcli/package-json') - -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} -if (process.argv.includes('--update-main-to-src')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'src/index.ts' })) - .then((pkg) => pkg.save()) -} diff --git a/packages/crypto/build.js b/packages/crypto/build.js index 5628aa4f4eb..e880ae9930b 100644 --- a/packages/crypto/build.js +++ b/packages/crypto/build.js @@ -1,16 +1,8 @@ -const pkgJson = require('@npmcli/package-json') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts'], diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 511d5aa105d..8bc10ae7f2d 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -2,6 +2,10 @@ "name": "@atproto/crypto", "version": "0.2.2", "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "license": "MIT", "repository": { "type": "git", @@ -13,15 +17,12 @@ "prettier": "prettier --check src/ tests/", "prettier:fix": "prettier --write src/ tests/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", + "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", - "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", - "update-main-to-src": "node ./update-pkg.js --update-main-to-src", - "prepublish": "npm run update-main-to-dist", - "postpublish": "npm run update-main-to-src" + "update-main-to-dist": "node ../../update-main-to-dist.js packages/crypto" }, "dependencies": { "@noble/curves": "^1.1.0", diff --git a/packages/crypto/update-pkg.js b/packages/crypto/update-pkg.js deleted file mode 100644 index 15d70bdab33..00000000000 --- a/packages/crypto/update-pkg.js +++ /dev/null @@ -1,14 +0,0 @@ -const pkgJson = require('@npmcli/package-json') - -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} -if (process.argv.includes('--update-main-to-src')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'src/index.ts' })) - .then((pkg) => pkg.save()) -} diff --git a/packages/dev-env/build.js b/packages/dev-env/build.js index a6cff485859..98bb2df6133 100644 --- a/packages/dev-env/build.js +++ b/packages/dev-env/build.js @@ -1,17 +1,9 @@ -const pkgJson = require('@npmcli/package-json') const { copy } = require('esbuild-plugin-copy') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts', 'src/bin.ts', 'src/bin-network.ts'], @@ -34,4 +26,4 @@ require('esbuild').build({ }, }), ]), -}) \ No newline at end of file +}) diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index 57b0f0b7adf..a1f9291d1f7 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -2,6 +2,10 @@ "name": "@atproto/dev-env", "version": "0.2.3", "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "bin": "dist/bin.js", "license": "MIT", "repository": { @@ -12,32 +16,31 @@ "scripts": { "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", + "update-main-to-dist": "node ../../update-main-to-dist.js packages/dev-env", "start": "node dist/bin.js", "start:network": "../dev-infra/with-test-redis-and-db.sh node dist/bin-network.js", "prettier": "prettier --check src/", "prettier:fix": "prettier --write src/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", - "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", - "update-main-to-src": "node ./update-pkg.js --update-main-to-src", - "prepublish": "npm run update-main-to-dist", - "postpublish": "npm run update-main-to-src" + "verify:fix": "pnpm prettier:fix && pnpm lint:fix" }, "dependencies": { - "@atproto/api": "*", - "@atproto/bsky": "*", - "@atproto/crypto": "*", - "@atproto/syntax": "*", - "@atproto/identity": "*", - "@atproto/pds": "*", - "@atproto/xrpc-server": "*", + "@atproto/api": "workspace:^", + "@atproto/bsky": "workspace:^", + "@atproto/common-web": "workspace:^", + "@atproto/crypto": "workspace:^", + "@atproto/identity": "workspace:^", + "@atproto/pds": "workspace:^", + "@atproto/syntax": "workspace:^", + "@atproto/xrpc-server": "workspace:^", "@did-plc/lib": "^0.0.1", "@did-plc/server": "^0.0.1", "better-sqlite3": "^7.6.2", "chalk": "^5.0.1", "dotenv": "^16.0.1", + "express": "^4.18.2", "get-port": "^6.1.2", "sharp": "^0.31.2", "uint8arrays": "3.0.0" diff --git a/packages/dev-env/update-pkg.js b/packages/dev-env/update-pkg.js deleted file mode 100644 index 15d70bdab33..00000000000 --- a/packages/dev-env/update-pkg.js +++ /dev/null @@ -1,14 +0,0 @@ -const pkgJson = require('@npmcli/package-json') - -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} -if (process.argv.includes('--update-main-to-src')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'src/index.ts' })) - .then((pkg) => pkg.save()) -} diff --git a/packages/identifier/build.js b/packages/identifier/build.js index 5628aa4f4eb..e880ae9930b 100644 --- a/packages/identifier/build.js +++ b/packages/identifier/build.js @@ -1,16 +1,8 @@ -const pkgJson = require('@npmcli/package-json') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts'], diff --git a/packages/identifier/package.json b/packages/identifier/package.json index 6abfe06cedd..c36e0feced7 100644 --- a/packages/identifier/package.json +++ b/packages/identifier/package.json @@ -2,20 +2,21 @@ "name": "@atproto/identifier", "version": "0.2.0", "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "scripts": { "test": "true", "prettier": "prettier --check src/", "prettier:fix": "prettier --write src/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", + "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", - "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", - "update-main-to-src": "node ./update-pkg.js --update-main-to-src", - "prepublish": "npm run update-main-to-dist", - "postpublish": "npm run update-main-to-src" + "update-main-to-dist": "node ../../update-main-to-dist.js packages/identifier" }, "license": "MIT", "repository": { @@ -24,6 +25,6 @@ "directory": "packages/identifier" }, "dependencies": { - "@atproto/syntax": "*" + "@atproto/syntax": "workspace:^" } } diff --git a/packages/identifier/update-pkg.js b/packages/identifier/update-pkg.js deleted file mode 100644 index 15d70bdab33..00000000000 --- a/packages/identifier/update-pkg.js +++ /dev/null @@ -1,14 +0,0 @@ -const pkgJson = require('@npmcli/package-json') - -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} -if (process.argv.includes('--update-main-to-src')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'src/index.ts' })) - .then((pkg) => pkg.save()) -} diff --git a/packages/identity/build.js b/packages/identity/build.js index 48de336eedb..709647b64ff 100644 --- a/packages/identity/build.js +++ b/packages/identity/build.js @@ -1,16 +1,8 @@ -const pkgJson = require('@npmcli/package-json') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts'], diff --git a/packages/identity/package.json b/packages/identity/package.json index bfe0e06b82b..5399698b1c3 100644 --- a/packages/identity/package.json +++ b/packages/identity/package.json @@ -2,6 +2,10 @@ "name": "@atproto/identity", "version": "0.2.0", "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "license": "MIT", "repository": { "type": "git", @@ -14,19 +18,16 @@ "prettier": "prettier --check src/ tests/", "prettier:fix": "prettier --write src/ tests/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", + "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", - "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", - "update-main-to-src": "node ./update-pkg.js --update-main-to-src", - "prepublish": "npm run update-main-to-dist", - "postpublish": "npm run update-main-to-src" + "update-main-to-dist": "node ../../update-main-to-dist.js packages/identity" }, "dependencies": { - "@atproto/common-web": "*", - "@atproto/crypto": "*", + "@atproto/common-web": "workspace:^", + "@atproto/crypto": "workspace:^", "axios": "^0.27.2", "zod": "^3.21.4" }, diff --git a/packages/identity/update-pkg.js b/packages/identity/update-pkg.js deleted file mode 100644 index 15d70bdab33..00000000000 --- a/packages/identity/update-pkg.js +++ /dev/null @@ -1,14 +0,0 @@ -const pkgJson = require('@npmcli/package-json') - -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} -if (process.argv.includes('--update-main-to-src')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'src/index.ts' })) - .then((pkg) => pkg.save()) -} diff --git a/packages/lex-cli/build.js b/packages/lex-cli/build.js index 5628aa4f4eb..e880ae9930b 100644 --- a/packages/lex-cli/build.js +++ b/packages/lex-cli/build.js @@ -1,16 +1,8 @@ -const pkgJson = require('@npmcli/package-json') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts'], diff --git a/packages/lex-cli/package.json b/packages/lex-cli/package.json index c521a70339a..23b85686031 100644 --- a/packages/lex-cli/package.json +++ b/packages/lex-cli/package.json @@ -5,15 +5,20 @@ "lex": "dist/index.js" }, "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/src/index.d.ts" + }, "scripts": { "prettier": "prettier --check src/", "prettier:fix": "prettier --write src/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", + "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", - "postbuild": "tsc --build tsconfig.build.json" + "postbuild": "tsc --build tsconfig.build.json", + "update-main-to-dist": "node ../../update-main-to-dist.js packages/lex-cli" }, "license": "MIT", "repository": { @@ -22,11 +27,12 @@ "directory": "packages/lex-cli" }, "dependencies": { - "@atproto/syntax": "*", - "@atproto/lexicon": "*", + "@atproto/lexicon": "workspace:^", + "@atproto/syntax": "workspace:^", "chalk": "^5.1.1", "commander": "^9.4.0", "ts-morph": "^16.0.0", - "yesno": "^0.4.0" + "yesno": "^0.4.0", + "zod": "^3.21.4" } } diff --git a/packages/lexicon/build.js b/packages/lexicon/build.js index 5628aa4f4eb..e880ae9930b 100644 --- a/packages/lexicon/build.js +++ b/packages/lexicon/build.js @@ -1,16 +1,8 @@ -const pkgJson = require('@npmcli/package-json') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts'], diff --git a/packages/lexicon/package.json b/packages/lexicon/package.json index 306991073f3..cfd10bd7246 100644 --- a/packages/lexicon/package.json +++ b/packages/lexicon/package.json @@ -2,20 +2,21 @@ "name": "@atproto/lexicon", "version": "0.2.0", "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "scripts": { "test": "jest", "prettier": "prettier --check src/ tests/", "prettier:fix": "prettier --write src/ tests/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", + "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", - "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", - "update-main-to-src": "node ./update-pkg.js --update-main-to-src", - "prepublish": "npm run update-main-to-dist", - "postpublish": "npm run update-main-to-src" + "update-main-to-dist": "node ../../update-main-to-dist.js packages/lexicon" }, "license": "MIT", "repository": { @@ -24,10 +25,10 @@ "directory": "packages/lexicon" }, "dependencies": { - "@atproto/common-web": "*", - "@atproto/syntax": "*", + "@atproto/common-web": "workspace:^", + "@atproto/syntax": "workspace:^", "iso-datestring-validator": "^2.2.2", - "multiformats": "^9.6.4", + "multiformats": "^9.9.0", "zod": "^3.21.4" } } diff --git a/packages/lexicon/update-pkg.js b/packages/lexicon/update-pkg.js deleted file mode 100644 index 15d70bdab33..00000000000 --- a/packages/lexicon/update-pkg.js +++ /dev/null @@ -1,14 +0,0 @@ -const pkgJson = require('@npmcli/package-json') - -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} -if (process.argv.includes('--update-main-to-src')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'src/index.ts' })) - .then((pkg) => pkg.save()) -} diff --git a/packages/nsid/build.js b/packages/nsid/build.js index 5628aa4f4eb..e880ae9930b 100644 --- a/packages/nsid/build.js +++ b/packages/nsid/build.js @@ -1,16 +1,8 @@ -const pkgJson = require('@npmcli/package-json') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts'], diff --git a/packages/nsid/package.json b/packages/nsid/package.json index 211d222826b..9f9e05c27f8 100644 --- a/packages/nsid/package.json +++ b/packages/nsid/package.json @@ -2,16 +2,21 @@ "name": "@atproto/nsid", "version": "0.1.0", "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/src/index.d.ts" + }, "scripts": { "test": "true", "prettier": "prettier --check src/", "prettier:fix": "prettier --write src/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", + "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", - "postbuild": "tsc --build tsconfig.build.json" + "postbuild": "tsc --build tsconfig.build.json", + "update-main-to-dist": "node ../../update-main-to-dist.js packages/nsid" }, "license": "MIT", "repository": { @@ -20,6 +25,6 @@ "directory": "packages/nsid" }, "dependencies": { - "@atproto/syntax": "*" + "@atproto/syntax": "workspace:^" } } diff --git a/packages/pds/build.js b/packages/pds/build.js index 99800275d50..82b7c051236 100644 --- a/packages/pds/build.js +++ b/packages/pds/build.js @@ -1,17 +1,9 @@ -const pkgJson = require('@npmcli/package-json') const { copy } = require('esbuild-plugin-copy') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts', 'src/bin.ts', 'src/db/index.ts'], diff --git a/packages/pds/package.json b/packages/pds/package.json index ce1f52fe464..e99b80be0b6 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -8,11 +8,16 @@ "directory": "packages/pds" }, "main": "src/index.ts", - "bin": "dist/bin.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/pds", "start": "node dist/bin.js", "test": "../dev-infra/with-test-redis-and-db.sh jest", "bench": "../dev-infra/with-test-redis-and-db.sh jest --config jest.bench.config.js", @@ -22,24 +27,21 @@ "prettier": "prettier --check src/ tests/", "prettier:fix": "prettier --write src/ tests/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", - "migration:create": "ts-node ./bin/migration-create.ts", - "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", - "update-main-to-src": "node ./update-pkg.js --update-main-to-src", - "prepublish": "npm run update-main-to-dist", - "postpublish": "npm run update-main-to-src" + "verify:fix": "pnpm prettier:fix && pnpm lint:fix", + "migration:create": "ts-node ./bin/migration-create.ts" }, "dependencies": { - "@atproto/api": "*", - "@atproto/common": "*", - "@atproto/crypto": "*", - "@atproto/syntax": "*", - "@atproto/identity": "*", - "@atproto/lexicon": "*", - "@atproto/repo": "*", - "@atproto/xrpc-server": "*", + "@atproto/api": "workspace:^", + "@atproto/common": "workspace:^", + "@atproto/crypto": "workspace:^", + "@atproto/syntax": "workspace:^", + "@atproto/identity": "workspace:^", + "@atproto/lexicon": "workspace:^", + "@atproto/repo": "workspace:^", + "@atproto/xrpc": "workspace:^", + "@atproto/xrpc-server": "workspace:^", "@did-plc/lib": "^0.0.1", "better-sqlite3": "^7.6.2", "bytes": "^3.1.2", @@ -58,26 +60,31 @@ "jsonwebtoken": "^8.5.1", "kysely": "^0.22.0", "lru-cache": "^10.0.1", - "multiformats": "^9.6.4", + "multiformats": "^9.9.0", "nodemailer": "^6.8.0", "nodemailer-html-to-text": "^3.2.0", "p-queue": "^6.6.2", "pg": "^8.10.0", - "pino": "^8.6.1", + "pino": "^8.15.0", "pino-http": "^8.2.1", "sharp": "^0.31.2", "typed-emitter": "^2.1.0", - "uint8arrays": "3.0.0" + "uint8arrays": "3.0.0", + "zod": "^3.21.4" }, "devDependencies": { - "@atproto/api": "*", - "@atproto/lex-cli": "*", + "@atproto/bsky": "workspace:^", + "@atproto/dev-env": "workspace:^", + "@atproto/api": "workspace:^", + "@atproto/lex-cli": "workspace:^", "@did-plc/server": "^0.0.1", "@types/cors": "^2.8.12", "@types/express": "^4.17.13", + "@types/express-serve-static-core": "^4.17.36", "@types/jsonwebtoken": "^8.5.9", "@types/nodemailer": "^6.4.6", "@types/pg": "^8.6.6", + "@types/qs": "^6.9.7", "@types/sharp": "^0.31.0", "axios": "^0.27.2" } diff --git a/packages/pds/service/package.json b/packages/pds/service/package.json deleted file mode 100644 index f783a050553..00000000000 --- a/packages/pds/service/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "plc-service", - "private": true, - "dependencies": { - "dd-trace": "^3.8.0" - } -} diff --git a/packages/pds/service/yarn.lock b/packages/pds/service/yarn.lock deleted file mode 100644 index c1090549f70..00000000000 --- a/packages/pds/service/yarn.lock +++ /dev/null @@ -1,320 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@datadog/native-appsec@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-2.0.0.tgz#ad65ba19bfd68e6b6c6cf64bb8ef55d099af8edc" - integrity sha512-XHARZ6MVgbnfOUO6/F3ZoZ7poXHJCNYFlgcyS2Xetuk9ITA5bfcooX2B2F7tReVB+RLJ+j8bsm0t55SyF04KDw== - dependencies: - node-gyp-build "^3.9.0" - -"@datadog/native-iast-rewriter@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-1.1.2.tgz#793cbf92d218ec80d645be0830023656b81018ea" - integrity sha512-pigRfRtAjZjMjqIXyXb98S4aDnuHz/EmqpoxAajFZsNjBLM87YonwSY5zoBdCsOyA46ddKOJRoCQd5ZalpOFMQ== - dependencies: - node-gyp-build "^4.5.0" - -"@datadog/native-iast-taint-tracking@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-1.1.0.tgz#8f7d0016157b32dbf5c01b15b8afb1c4286b4a18" - integrity sha512-TOrngpt6Qh52zWFOz1CkFXw0g43rnuUziFBtIMUsOLGzSHr9wdnTnE6HAyuvKy3f3ecAoZESlMfilGRKP93hXQ== - dependencies: - node-gyp-build "^3.9.0" - -"@datadog/native-metrics@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@datadog/native-metrics/-/native-metrics-1.5.0.tgz#e71b6b6d65f4bd58dfdffab2737890e8eef34584" - integrity sha512-K63XMDx74RLhOpM8I9GGZR9ft0CNNB/RkjYPLHcVGvVnBR47zmWE2KFa7Yrtzjbk73+88PXI4nzqLyR3PJsaIQ== - dependencies: - node-gyp-build "^3.9.0" - -"@datadog/pprof@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-1.1.1.tgz#17e86035140523ac3a96f3662e5dd29822042d61" - integrity sha512-5lYXUpikQhrJwzODtJ7aFM0oKmPccISnTCecuWhjxIj4/7UJv0DamkLak634bgEW+kiChgkKFDapHSesuXRDXQ== - dependencies: - delay "^5.0.0" - findit2 "^2.2.3" - node-gyp-build "^3.9.0" - p-limit "^3.1.0" - pify "^5.0.0" - protobufjs "^7.0.0" - source-map "^0.7.3" - split "^1.0.1" - -"@datadog/sketches-js@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@datadog/sketches-js/-/sketches-js-2.1.0.tgz#8c7e8028a5fc22ad102fa542b0a446c956830455" - integrity sha512-smLocSfrt3s53H/XSVP3/1kP42oqvrkjUPtyaFd1F79ux24oE31BKt+q0c6lsa6hOYrFzsIwyc5GXAI5JmfOew== - -"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" - integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== - -"@protobufjs/base64@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" - integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== - -"@protobufjs/codegen@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" - integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== - -"@protobufjs/eventemitter@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" - integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== - -"@protobufjs/fetch@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" - integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== - dependencies: - "@protobufjs/aspromise" "^1.1.1" - "@protobufjs/inquire" "^1.1.0" - -"@protobufjs/float@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" - integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== - -"@protobufjs/inquire@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" - integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== - -"@protobufjs/path@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" - integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== - -"@protobufjs/pool@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" - integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== - -"@protobufjs/utf8@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" - integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== - -"@types/node@>=13.7.0": - version "18.13.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850" - integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg== - -crypto-randomuuid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/crypto-randomuuid/-/crypto-randomuuid-1.0.0.tgz#acf583e5e085e867ae23e107ff70279024f9e9e7" - integrity sha512-/RC5F4l1SCqD/jazwUF6+t34Cd8zTSAGZ7rvvZu1whZUhD2a5MOGKjSGowoGcpj/cbVZk1ZODIooJEQQq3nNAA== - -dd-trace@^3.8.0: - version "3.13.2" - resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-3.13.2.tgz#95b1ec480ab9ac406e1da7591a8c6f678d3799fd" - integrity sha512-POO9nEcAufe5pgp2xV1X3PfWip6wh+6TpEcRSlSgZJCIIMvWVCkcIVL/J2a6KAZq6V3Yjbkl8Ktfe+MOzQf5kw== - dependencies: - "@datadog/native-appsec" "2.0.0" - "@datadog/native-iast-rewriter" "1.1.2" - "@datadog/native-iast-taint-tracking" "1.1.0" - "@datadog/native-metrics" "^1.5.0" - "@datadog/pprof" "^1.1.1" - "@datadog/sketches-js" "^2.1.0" - crypto-randomuuid "^1.0.0" - diagnostics_channel "^1.1.0" - ignore "^5.2.0" - import-in-the-middle "^1.3.4" - ipaddr.js "^2.0.1" - istanbul-lib-coverage "3.2.0" - koalas "^1.0.2" - limiter "^1.1.4" - lodash.kebabcase "^4.1.1" - lodash.pick "^4.4.0" - lodash.sortby "^4.7.0" - lodash.uniq "^4.5.0" - lru-cache "^7.14.0" - methods "^1.1.2" - module-details-from-path "^1.0.3" - node-abort-controller "^3.0.1" - opentracing ">=0.12.1" - path-to-regexp "^0.1.2" - protobufjs "^7.1.2" - retry "^0.10.1" - semver "^5.5.0" - -delay@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" - integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== - -diagnostics_channel@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/diagnostics_channel/-/diagnostics_channel-1.1.0.tgz#bd66c49124ce3bac697dff57466464487f57cea5" - integrity sha512-OE1ngLDjSBPG6Tx0YATELzYzy3RKHC+7veQ8gLa8yS7AAgw65mFbVdcsu3501abqOZCEZqZyAIemB0zXlqDSuw== - -findit2@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/findit2/-/findit2-2.2.3.tgz#58a466697df8a6205cdfdbf395536b8bd777a5f6" - integrity sha512-lg/Moejf4qXovVutL0Lz4IsaPoNYMuxt4PA0nGqFxnJ1CTTGGlEO2wKgoDpwknhvZ8k4Q2F+eesgkLbG2Mxfog== - -ignore@^5.2.0: - version "5.2.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" - integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== - -import-in-the-middle@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.3.4.tgz#7074bbd4e84e8cdafd1eae400b04e6fe252a0768" - integrity sha512-TUXqqEFacJ2DWAeYOhHwGZTMJtFxFVw0C1pYA+AXmuWXZGnBqUhHdtVrSkSbW5D7k2yriBG45j23iH9TRtI+bQ== - dependencies: - module-details-from-path "^1.0.3" - -ipaddr.js@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" - integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== - -istanbul-lib-coverage@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" - integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== - -koalas@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/koalas/-/koalas-1.0.2.tgz#318433f074235db78fae5661a02a8ca53ee295cd" - integrity sha512-RYhBbYaTTTHId3l6fnMZc3eGQNW6FVCqMG6AMwA5I1Mafr6AflaXeoi6x3xQuATRotGYRLk6+1ELZH4dstFNOA== - -limiter@^1.1.4: - version "1.1.5" - resolved "https://registry.yarnpkg.com/limiter/-/limiter-1.1.5.tgz#8f92a25b3b16c6131293a0cc834b4a838a2aa7c2" - integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== - -lodash.kebabcase@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" - integrity sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g== - -lodash.pick@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" - integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== - -lodash.sortby@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" - integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== - -lodash.uniq@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== - -long@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/long/-/long-5.2.1.tgz#e27595d0083d103d2fa2c20c7699f8e0c92b897f" - integrity sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A== - -lru-cache@^7.14.0: - version "7.16.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.16.0.tgz#b1b946cff368d3f3c569cc3d6a5ba8f90435160f" - integrity sha512-VJBdeMa9Bz27NNlx+DI/YXGQtXdjUU+9gdfN1rYfra7vtTjhodl5tVNmR42bo+ORHuDqDT+lGAUAb+lzvY42Bw== - -methods@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== - -module-details-from-path@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" - integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A== - -node-abort-controller@^3.0.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" - integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== - -node-gyp-build@^3.9.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.9.0.tgz#53a350187dd4d5276750da21605d1cb681d09e25" - integrity sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A== - -node-gyp-build@^4.5.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" - integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== - -opentracing@>=0.12.1: - version "0.14.7" - resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.14.7.tgz#25d472bd0296dc0b64d7b94cbc995219031428f5" - integrity sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q== - -p-limit@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -path-to-regexp@^0.1.2: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== - -pify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" - integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== - -protobufjs@^7.0.0, protobufjs@^7.1.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.2.tgz#2af401d8c547b9476fb37ffc65782cf302342ca3" - integrity sha512-++PrQIjrom+bFDPpfmqXfAGSQs40116JRrqqyf53dymUMvvb5d/LMRyicRoF1AUKoXVS1/IgJXlEgcpr4gTF3Q== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/node" ">=13.7.0" - long "^5.0.0" - -retry@^0.10.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" - integrity sha512-ZXUSQYTHdl3uS7IuCehYfMzKyIDBNoAuUblvy5oGO5UJSUTmStUUVPXbA9Qxd173Bgre53yCQczQuHgRWAdvJQ== - -semver@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -source-map@^0.7.3: - version "0.7.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== - -split@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" - integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== - dependencies: - through "2" - -through@2: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/packages/pds/update-pkg.js b/packages/pds/update-pkg.js deleted file mode 100644 index 15d70bdab33..00000000000 --- a/packages/pds/update-pkg.js +++ /dev/null @@ -1,14 +0,0 @@ -const pkgJson = require('@npmcli/package-json') - -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} -if (process.argv.includes('--update-main-to-src')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'src/index.ts' })) - .then((pkg) => pkg.save()) -} diff --git a/packages/repo/build.js b/packages/repo/build.js index 5628aa4f4eb..e880ae9930b 100644 --- a/packages/repo/build.js +++ b/packages/repo/build.js @@ -1,16 +1,8 @@ -const pkgJson = require('@npmcli/package-json') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts'], diff --git a/packages/repo/package.json b/packages/repo/package.json index e879099c21c..96dc165f56f 100644 --- a/packages/repo/package.json +++ b/packages/repo/package.json @@ -2,6 +2,10 @@ "name": "@atproto/repo", "version": "0.3.0", "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "license": "MIT", "repository": { "type": "git", @@ -16,25 +20,23 @@ "prettier": "prettier --check src/ tests/", "prettier:fix": "prettier --write src/ tests/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", + "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", - "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", - "update-main-to-src": "node ./update-pkg.js --update-main-to-src", - "prepublish": "npm run update-main-to-dist", - "postpublish": "npm run update-main-to-src" + "update-main-to-dist": "node ../../update-main-to-dist.js packages/repo" }, "dependencies": { - "@atproto/common": "*", - "@atproto/crypto": "*", - "@atproto/identity": "*", - "@atproto/lexicon": "*", - "@atproto/syntax": "*", + "@atproto/common": "workspace:^", + "@atproto/common-web": "workspace:^", + "@atproto/crypto": "workspace:^", + "@atproto/identity": "workspace:^", + "@atproto/lexicon": "workspace:^", + "@atproto/syntax": "workspace:^", "@ipld/car": "^3.2.3", "@ipld/dag-cbor": "^7.0.0", - "multiformats": "^9.6.4", + "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "zod": "^3.21.4" } diff --git a/packages/repo/src/logger.ts b/packages/repo/src/logger.ts index c4dabaac451..d4d1fdb9936 100644 --- a/packages/repo/src/logger.ts +++ b/packages/repo/src/logger.ts @@ -1,5 +1,6 @@ import { subsystemLogger } from '@atproto/common' -export const logger = subsystemLogger('repo') +export const logger: ReturnType = + subsystemLogger('repo') export default logger diff --git a/packages/repo/src/types.ts b/packages/repo/src/types.ts index e3523a3ba87..4b796193ef3 100644 --- a/packages/repo/src/types.ts +++ b/packages/repo/src/types.ts @@ -1,5 +1,6 @@ import { z } from 'zod' -import { schema as common, def as commonDef } from '@atproto/common' +import { def as commonDef } from '@atproto/common-web' +import { schema as common } from '@atproto/common' import { CID } from 'multiformats' import BlockMap from './block-map' import { RepoRecord } from '@atproto/lexicon' diff --git a/packages/repo/update-pkg.js b/packages/repo/update-pkg.js deleted file mode 100644 index 15d70bdab33..00000000000 --- a/packages/repo/update-pkg.js +++ /dev/null @@ -1,14 +0,0 @@ -const pkgJson = require('@npmcli/package-json') - -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} -if (process.argv.includes('--update-main-to-src')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'src/index.ts' })) - .then((pkg) => pkg.save()) -} diff --git a/packages/syntax/package.json b/packages/syntax/package.json index 9a257b9bb4a..943123e5946 100644 --- a/packages/syntax/package.json +++ b/packages/syntax/package.json @@ -2,20 +2,21 @@ "name": "@atproto/syntax", "version": "0.1.0", "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "scripts": { "test": "jest", "prettier": "prettier --check src/ tests/", "prettier:fix": "prettier --write src/ tests/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", + "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", - "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", - "update-main-to-src": "node ./update-pkg.js --update-main-to-src", - "prepublish": "npm run update-main-to-dist", - "postpublish": "npm run update-main-to-src" + "update-main-to-dist": "node ../../update-main-to-dist.js packages/syntax" }, "license": "MIT", "repository": { @@ -24,7 +25,7 @@ "directory": "packages/syntax" }, "dependencies": { - "@atproto/common-web": "*" + "@atproto/common-web": "workspace:^" }, "browser": { "dns/promises": false diff --git a/packages/syntax/update-pkg.js b/packages/syntax/update-pkg.js deleted file mode 100644 index 15d70bdab33..00000000000 --- a/packages/syntax/update-pkg.js +++ /dev/null @@ -1,14 +0,0 @@ -const pkgJson = require('@npmcli/package-json') - -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} -if (process.argv.includes('--update-main-to-src')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'src/index.ts' })) - .then((pkg) => pkg.save()) -} diff --git a/packages/uri/build.js b/packages/uri/build.js index 5628aa4f4eb..e880ae9930b 100644 --- a/packages/uri/build.js +++ b/packages/uri/build.js @@ -1,16 +1,8 @@ -const pkgJson = require('@npmcli/package-json') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts'], diff --git a/packages/uri/package.json b/packages/uri/package.json index 82e96c1a0ae..10b91e7f267 100644 --- a/packages/uri/package.json +++ b/packages/uri/package.json @@ -2,20 +2,21 @@ "name": "@atproto/uri", "version": "0.1.0", "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "scripts": { "test": "true", "prettier": "prettier --check src/", "prettier:fix": "prettier --write src/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", + "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", - "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", - "update-main-to-src": "node ./update-pkg.js --update-main-to-src", - "prepublish": "npm run update-main-to-dist", - "postpublish": "npm run update-main-to-src" + "update-main-to-dist": "node ../../update-main-to-dist.js packages/uri" }, "license": "MIT", "repository": { @@ -24,6 +25,6 @@ "directory": "packages/uri" }, "dependencies": { - "@atproto/syntax": "*" + "@atproto/syntax": "workspace:^" } } diff --git a/packages/uri/update-pkg.js b/packages/uri/update-pkg.js deleted file mode 100644 index 15d70bdab33..00000000000 --- a/packages/uri/update-pkg.js +++ /dev/null @@ -1,14 +0,0 @@ -const pkgJson = require('@npmcli/package-json') - -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} -if (process.argv.includes('--update-main-to-src')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'src/index.ts' })) - .then((pkg) => pkg.save()) -} diff --git a/packages/xrpc-server/build.js b/packages/xrpc-server/build.js index 5628aa4f4eb..e880ae9930b 100644 --- a/packages/xrpc-server/build.js +++ b/packages/xrpc-server/build.js @@ -1,16 +1,8 @@ -const pkgJson = require('@npmcli/package-json') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts'], diff --git a/packages/xrpc-server/package.json b/packages/xrpc-server/package.json index af6e84b217a..4901b53f638 100644 --- a/packages/xrpc-server/package.json +++ b/packages/xrpc-server/package.json @@ -2,20 +2,21 @@ "name": "@atproto/xrpc-server", "version": "0.3.0", "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "scripts": { "test": "jest", "prettier": "prettier --check src/ tests/", "prettier:fix": "prettier --write src/ tests/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", + "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", - "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", - "update-main-to-src": "node ./update-pkg.js --update-main-to-src", - "prepublish": "npm run update-main-to-dist", - "postpublish": "npm run update-main-to-src" + "update-main-to-dist": "node ../../update-main-to-dist.js packages/xrpc-server" }, "license": "MIT", "repository": { @@ -24,9 +25,9 @@ "directory": "packages/xrpc-server" }, "dependencies": { - "@atproto/common": "*", - "@atproto/crypto": "*", - "@atproto/lexicon": "*", + "@atproto/common": "workspace:^", + "@atproto/crypto": "workspace:^", + "@atproto/lexicon": "workspace:^", "cbor-x": "^1.5.1", "express": "^4.17.2", "http-errors": "^2.0.0", @@ -37,12 +38,13 @@ "zod": "^3.21.4" }, "devDependencies": { - "@atproto/crypto": "*", - "@atproto/xrpc": "*", + "@atproto/crypto": "workspace:^", + "@atproto/xrpc": "workspace:^", "@types/express": "^4.17.13", + "@types/express-serve-static-core": "^4.17.36", "@types/http-errors": "^2.0.1", "@types/ws": "^8.5.4", "get-port": "^6.1.2", - "multiformats": "^9.6.4" + "multiformats": "^9.9.0" } } diff --git a/packages/xrpc-server/src/logger.ts b/packages/xrpc-server/src/logger.ts index 87ec0bd1ae0..1e8599637e0 100644 --- a/packages/xrpc-server/src/logger.ts +++ b/packages/xrpc-server/src/logger.ts @@ -1,5 +1,6 @@ import { subsystemLogger } from '@atproto/common' -export const logger = subsystemLogger('xrpc-server') +export const logger: ReturnType = + subsystemLogger('xrpc-server') export default logger diff --git a/packages/xrpc-server/src/stream/logger.ts b/packages/xrpc-server/src/stream/logger.ts index 952d2cb8fc9..7cff29b9ade 100644 --- a/packages/xrpc-server/src/stream/logger.ts +++ b/packages/xrpc-server/src/stream/logger.ts @@ -1,5 +1,6 @@ import { subsystemLogger } from '@atproto/common' -export const logger = subsystemLogger('xrpc-stream') +export const logger: ReturnType = + subsystemLogger('xrpc-stream') export default logger diff --git a/packages/xrpc-server/update-pkg.js b/packages/xrpc-server/update-pkg.js deleted file mode 100644 index 15d70bdab33..00000000000 --- a/packages/xrpc-server/update-pkg.js +++ /dev/null @@ -1,14 +0,0 @@ -const pkgJson = require('@npmcli/package-json') - -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} -if (process.argv.includes('--update-main-to-src')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'src/index.ts' })) - .then((pkg) => pkg.save()) -} diff --git a/packages/xrpc/build.js b/packages/xrpc/build.js index 5628aa4f4eb..e880ae9930b 100644 --- a/packages/xrpc/build.js +++ b/packages/xrpc/build.js @@ -1,16 +1,8 @@ -const pkgJson = require('@npmcli/package-json') const { nodeExternalsPlugin } = require('esbuild-node-externals') const buildShallow = process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true' -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} - require('esbuild').build({ logLevel: 'info', entryPoints: ['src/index.ts'], diff --git a/packages/xrpc/package.json b/packages/xrpc/package.json index 5f45f71f95f..2949497b5ca 100644 --- a/packages/xrpc/package.json +++ b/packages/xrpc/package.json @@ -2,19 +2,20 @@ "name": "@atproto/xrpc", "version": "0.3.0", "main": "src/index.ts", + "publishConfig": { + "main": "dist/index.js", + "types": "dist/index.d.ts" + }, "scripts": { "prettier": "prettier --check src/", "prettier:fix": "prettier --write src/", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "yarn lint --fix", + "lint:fix": "pnpm lint --fix", "verify": "run-p prettier lint", - "verify:fix": "yarn prettier:fix && yarn lint:fix", + "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", - "update-main-to-dist": "node ./update-pkg.js --update-main-to-dist", - "update-main-to-src": "node ./update-pkg.js --update-main-to-src", - "prepublish": "npm run update-main-to-dist", - "postpublish": "npm run update-main-to-src" + "update-main-to-dist": "node ../../update-main-to-dist.js packages/xrpc" }, "license": "MIT", "repository": { @@ -23,7 +24,7 @@ "directory": "packages/xrpc" }, "dependencies": { - "@atproto/lexicon": "*", + "@atproto/lexicon": "workspace:^", "zod": "^3.21.4" } } diff --git a/packages/xrpc/update-pkg.js b/packages/xrpc/update-pkg.js deleted file mode 100644 index 15d70bdab33..00000000000 --- a/packages/xrpc/update-pkg.js +++ /dev/null @@ -1,14 +0,0 @@ -const pkgJson = require('@npmcli/package-json') - -if (process.argv.includes('--update-main-to-dist')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'dist/index.js' })) - .then((pkg) => pkg.save()) -} -if (process.argv.includes('--update-main-to-src')) { - return pkgJson - .load(__dirname) - .then((pkg) => pkg.update({ main: 'src/index.ts' })) - .then((pkg) => pkg.save()) -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000000..b18c4765c4a --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,10611 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@babel/core': + specifier: ^7.18.6 + version: 7.18.6 + '@babel/preset-env': + specifier: ^7.18.6 + version: 7.18.6(@babel/core@7.18.6) + '@npmcli/package-json': + specifier: ^3.0.0 + version: 3.0.0 + '@swc/core': + specifier: ^1.3.42 + version: 1.3.42 + '@swc/jest': + specifier: ^0.2.24 + version: 0.2.24(@swc/core@1.3.42) + '@types/jest': + specifier: ^28.1.4 + version: 28.1.4 + '@types/node': + specifier: ^18.0.0 + version: 18.0.0 + '@typescript-eslint/eslint-plugin': + specifier: ^5.38.1 + version: 5.38.1(@typescript-eslint/parser@5.38.1)(eslint@8.24.0)(typescript@4.8.4) + '@typescript-eslint/parser': + specifier: ^5.38.1 + version: 5.38.1(eslint@8.24.0)(typescript@4.8.4) + babel-eslint: + specifier: ^10.1.0 + version: 10.1.0(eslint@8.24.0) + dotenv: + specifier: ^16.0.3 + version: 16.0.3 + esbuild: + specifier: ^0.14.48 + version: 0.14.48 + esbuild-node-externals: + specifier: ^1.5.0 + version: 1.5.0(esbuild@0.14.48) + esbuild-plugin-copy: + specifier: ^1.6.0 + version: 1.6.0(esbuild@0.14.48) + eslint: + specifier: ^8.24.0 + version: 8.24.0 + eslint-config-prettier: + specifier: ^8.5.0 + version: 8.5.0(eslint@8.24.0) + eslint-plugin-prettier: + specifier: ^4.2.1 + version: 4.2.1(eslint-config-prettier@8.5.0)(eslint@8.24.0)(prettier@2.7.1) + jest: + specifier: ^28.1.2 + version: 28.1.2(@types/node@18.0.0)(ts-node@10.8.2) + node-gyp: + specifier: ^9.3.1 + version: 9.3.1 + npm-run-all: + specifier: ^4.1.5 + version: 4.1.5 + pino-pretty: + specifier: ^9.1.0 + version: 9.1.0 + prettier: + specifier: ^2.7.1 + version: 2.7.1 + prettier-config-standard: + specifier: ^5.0.0 + version: 5.0.0(prettier@2.7.1) + ts-node: + specifier: ^10.8.2 + version: 10.8.2(@swc/core@1.3.42)(@types/node@18.0.0)(typescript@4.8.4) + typescript: + specifier: ^4.8.4 + version: 4.8.4 + + packages/api: + dependencies: + '@atproto/common-web': + specifier: workspace:^ + version: link:../common-web + '@atproto/lexicon': + specifier: workspace:^ + version: link:../lexicon + '@atproto/syntax': + specifier: workspace:^ + version: link:../syntax + '@atproto/xrpc': + specifier: workspace:^ + version: link:../xrpc + multiformats: + specifier: ^9.9.0 + version: 9.9.0 + tlds: + specifier: ^1.234.0 + version: 1.234.0 + typed-emitter: + specifier: ^2.1.0 + version: 2.1.0 + devDependencies: + '@atproto/lex-cli': + specifier: workspace:^ + version: link:../lex-cli + '@atproto/pds': + specifier: workspace:^ + version: link:../pds + common-tags: + specifier: ^1.8.2 + version: 1.8.2 + + packages/aws: + dependencies: + '@atproto/crypto': + specifier: workspace:^ + version: link:../crypto + '@atproto/repo': + specifier: workspace:^ + version: link:../repo + '@aws-sdk/client-cloudfront': + specifier: ^3.261.0 + version: 3.261.0 + '@aws-sdk/client-kms': + specifier: ^3.196.0 + version: 3.196.0 + '@aws-sdk/client-s3': + specifier: ^3.224.0 + version: 3.224.0 + '@aws-sdk/lib-storage': + specifier: ^3.226.0 + version: 3.226.0(@aws-sdk/abort-controller@3.374.0)(@aws-sdk/client-s3@3.224.0) + '@noble/curves': + specifier: ^1.1.0 + version: 1.1.0 + key-encoder: + specifier: ^2.0.3 + version: 2.0.3 + multiformats: + specifier: ^9.9.0 + version: 9.9.0 + uint8arrays: + specifier: 3.0.0 + version: 3.0.0 + + packages/bsky: + 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/repo': + specifier: workspace:^ + version: link:../repo + '@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 + '@isaacs/ttlcache': + specifier: ^1.4.1 + version: 1.4.1 + compression: + specifier: ^1.7.4 + version: 1.7.4 + cors: + specifier: ^2.8.5 + version: 2.8.5 + dotenv: + specifier: ^16.0.0 + version: 16.0.3 + express: + specifier: ^4.17.2 + version: 4.18.2 + express-async-errors: + specifier: ^3.1.1 + version: 3.1.1(express@4.18.2) + form-data: + specifier: ^4.0.0 + version: 4.0.0 + http-errors: + specifier: ^2.0.0 + version: 2.0.0 + http-terminator: + specifier: ^3.2.0 + version: 3.2.0 + ioredis: + specifier: ^5.3.2 + version: 5.3.2 + iso-datestring-validator: + specifier: ^2.2.2 + version: 2.2.2 + kysely: + specifier: ^0.22.0 + version: 0.22.0 + 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.2.1 + sharp: + specifier: ^0.31.2 + version: 0.31.2 + 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 + '@types/sharp': + specifier: ^0.31.0 + version: 0.31.0 + axios: + specifier: ^0.27.2 + version: 0.27.2 + + packages/common: + dependencies: + '@atproto/common-web': + specifier: workspace:^ + version: link:../common-web + '@ipld/dag-cbor': + specifier: ^7.0.3 + version: 7.0.3 + cbor-x: + specifier: ^1.5.1 + version: 1.5.1 + multiformats: + specifier: ^9.9.0 + version: 9.9.0 + pino: + specifier: ^8.15.0 + version: 8.15.0 + zod: + specifier: 3.21.4 + version: 3.21.4 + + packages/common-web: + dependencies: + graphemer: + specifier: ^1.4.0 + version: 1.4.0 + multiformats: + specifier: ^9.9.0 + version: 9.9.0 + uint8arrays: + specifier: 3.0.0 + version: 3.0.0 + zod: + specifier: ^3.21.4 + version: 3.21.4 + + packages/crypto: + dependencies: + '@noble/curves': + specifier: ^1.1.0 + version: 1.1.0 + '@noble/hashes': + specifier: ^1.3.1 + version: 1.3.1 + uint8arrays: + specifier: 3.0.0 + version: 3.0.0 + + packages/dev-env: + dependencies: + '@atproto/api': + specifier: workspace:^ + version: link:../api + '@atproto/bsky': + specifier: workspace:^ + version: link:../bsky + '@atproto/common-web': + specifier: workspace:^ + version: link:../common-web + '@atproto/crypto': + specifier: workspace:^ + version: link:../crypto + '@atproto/identity': + specifier: workspace:^ + version: link:../identity + '@atproto/pds': + specifier: workspace:^ + version: link:../pds + '@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 + '@did-plc/server': + specifier: ^0.0.1 + version: 0.0.1 + better-sqlite3: + specifier: ^7.6.2 + version: 7.6.2 + chalk: + specifier: ^5.0.1 + version: 5.1.1 + dotenv: + specifier: ^16.0.1 + version: 16.0.3 + express: + specifier: ^4.18.2 + version: 4.18.2 + get-port: + specifier: ^6.1.2 + version: 6.1.2 + sharp: + specifier: ^0.31.2 + version: 0.31.2 + uint8arrays: + specifier: 3.0.0 + version: 3.0.0 + devDependencies: + ts-node: + specifier: ^10.8.1 + version: 10.8.2(@swc/core@1.3.42)(@types/node@18.17.8)(typescript@4.9.5) + + packages/identifier: + dependencies: + '@atproto/syntax': + specifier: workspace:^ + version: link:../syntax + + packages/identity: + dependencies: + '@atproto/common-web': + specifier: workspace:^ + version: link:../common-web + '@atproto/crypto': + specifier: workspace:^ + version: link:../crypto + axios: + specifier: ^0.27.2 + version: 0.27.2 + zod: + specifier: ^3.21.4 + version: 3.21.4 + devDependencies: + '@did-plc/lib': + specifier: ^0.0.1 + version: 0.0.1 + '@did-plc/server': + specifier: ^0.0.1 + version: 0.0.1 + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^4.18.2 + version: 4.18.2 + get-port: + specifier: ^6.1.2 + version: 6.1.2 + + packages/lex-cli: + dependencies: + '@atproto/lexicon': + specifier: workspace:^ + version: link:../lexicon + '@atproto/syntax': + specifier: workspace:^ + version: link:../syntax + chalk: + specifier: ^5.1.1 + version: 5.1.1 + commander: + specifier: ^9.4.0 + version: 9.4.0 + ts-morph: + specifier: ^16.0.0 + version: 16.0.0 + yesno: + specifier: ^0.4.0 + version: 0.4.0 + zod: + specifier: ^3.21.4 + version: 3.21.4 + + packages/lexicon: + dependencies: + '@atproto/common-web': + specifier: workspace:^ + version: link:../common-web + '@atproto/syntax': + specifier: workspace:^ + version: link:../syntax + iso-datestring-validator: + specifier: ^2.2.2 + version: 2.2.2 + multiformats: + specifier: ^9.9.0 + version: 9.9.0 + zod: + specifier: ^3.21.4 + version: 3.21.4 + + packages/nsid: + dependencies: + '@atproto/syntax': + specifier: workspace:^ + version: link:../syntax + + packages/pds: + 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/repo': + specifier: workspace:^ + version: link:../repo + '@atproto/syntax': + specifier: workspace:^ + version: link:../syntax + '@atproto/xrpc': + specifier: workspace:^ + version: link:../xrpc + '@atproto/xrpc-server': + specifier: workspace:^ + version: link:../xrpc-server + '@did-plc/lib': + specifier: ^0.0.1 + version: 0.0.1 + better-sqlite3: + specifier: ^7.6.2 + version: 7.6.2 + bytes: + specifier: ^3.1.2 + version: 3.1.2 + compression: + specifier: ^1.7.4 + version: 1.7.4 + cors: + specifier: ^2.8.5 + version: 2.8.5 + dotenv: + specifier: ^16.0.0 + version: 16.0.3 + express: + specifier: ^4.17.2 + version: 4.18.2 + express-async-errors: + specifier: ^3.1.1 + version: 3.1.1(express@4.18.2) + file-type: + specifier: ^16.5.4 + version: 16.5.4 + form-data: + specifier: ^4.0.0 + version: 4.0.0 + handlebars: + specifier: ^4.7.7 + version: 4.7.7 + http-errors: + specifier: ^2.0.0 + version: 2.0.0 + http-terminator: + specifier: ^3.2.0 + version: 3.2.0 + ioredis: + specifier: ^5.3.2 + version: 5.3.2 + iso-datestring-validator: + specifier: ^2.2.2 + version: 2.2.2 + jsonwebtoken: + specifier: ^8.5.1 + version: 8.5.1 + kysely: + specifier: ^0.22.0 + version: 0.22.0 + lru-cache: + specifier: ^10.0.1 + version: 10.0.1 + multiformats: + specifier: ^9.9.0 + version: 9.9.0 + nodemailer: + specifier: ^6.8.0 + version: 6.8.0 + nodemailer-html-to-text: + specifier: ^3.2.0 + version: 3.2.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.2.1 + sharp: + specifier: ^0.31.2 + version: 0.31.2 + typed-emitter: + specifier: ^2.1.0 + version: 2.1.0 + uint8arrays: + specifier: 3.0.0 + version: 3.0.0 + zod: + specifier: ^3.21.4 + version: 3.21.4 + devDependencies: + '@atproto/bsky': + specifier: workspace:^ + version: link:../bsky + '@atproto/dev-env': + specifier: workspace:^ + version: link:../dev-env + '@atproto/lex-cli': + specifier: workspace:^ + version: link:../lex-cli + '@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/jsonwebtoken': + specifier: ^8.5.9 + version: 8.5.9 + '@types/nodemailer': + specifier: ^6.4.6 + version: 6.4.6 + '@types/pg': + specifier: ^8.6.6 + version: 8.6.6 + '@types/qs': + specifier: ^6.9.7 + version: 6.9.7 + '@types/sharp': + specifier: ^0.31.0 + version: 0.31.0 + axios: + specifier: ^0.27.2 + version: 0.27.2 + + packages/repo: + dependencies: + '@atproto/common': + specifier: workspace:^ + version: link:../common + '@atproto/common-web': + specifier: workspace:^ + version: link:../common-web + '@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 + '@ipld/car': + specifier: ^3.2.3 + version: 3.2.3 + '@ipld/dag-cbor': + specifier: ^7.0.0 + version: 7.0.3 + multiformats: + specifier: ^9.9.0 + version: 9.9.0 + uint8arrays: + specifier: 3.0.0 + version: 3.0.0 + zod: + specifier: ^3.21.4 + version: 3.21.4 + + packages/syntax: + dependencies: + '@atproto/common-web': + specifier: workspace:^ + version: link:../common-web + + packages/uri: + dependencies: + '@atproto/syntax': + specifier: workspace:^ + version: link:../syntax + + packages/xrpc: + dependencies: + '@atproto/lexicon': + specifier: workspace:^ + version: link:../lexicon + zod: + specifier: ^3.21.4 + version: 3.21.4 + + packages/xrpc-server: + dependencies: + '@atproto/common': + specifier: workspace:^ + version: link:../common + '@atproto/crypto': + specifier: workspace:^ + version: link:../crypto + '@atproto/lexicon': + specifier: workspace:^ + version: link:../lexicon + cbor-x: + specifier: ^1.5.1 + version: 1.5.1 + express: + specifier: ^4.17.2 + version: 4.18.2 + http-errors: + specifier: ^2.0.0 + version: 2.0.0 + mime-types: + specifier: ^2.1.35 + version: 2.1.35 + rate-limiter-flexible: + specifier: ^2.4.1 + version: 2.4.1 + uint8arrays: + specifier: 3.0.0 + version: 3.0.0 + ws: + specifier: ^8.12.0 + version: 8.12.0 + zod: + specifier: ^3.21.4 + version: 3.21.4 + devDependencies: + '@atproto/xrpc': + specifier: workspace:^ + version: link:../xrpc + '@types/express': + specifier: ^4.17.13 + version: 4.17.13 + '@types/express-serve-static-core': + specifier: ^4.17.36 + version: 4.17.36 + '@types/http-errors': + specifier: ^2.0.1 + version: 2.0.1 + '@types/ws': + specifier: ^8.5.4 + version: 8.5.4 + get-port: + specifier: ^6.1.2 + version: 6.1.2 + multiformats: + specifier: ^9.9.0 + version: 9.9.0 + + services/bsky: + dependencies: + '@atproto/aws': + specifier: workspace:^ + version: link:../../packages/aws + '@atproto/bsky': + specifier: workspace:^ + version: link:../../packages/bsky + dd-trace: + specifier: 3.13.2 + version: 3.13.2 + + services/pds: + dependencies: + '@atproto/aws': + specifier: workspace:^ + version: link:../../packages/aws + '@atproto/crypto': + specifier: workspace:^ + version: link:../../packages/crypto + '@atproto/pds': + specifier: workspace:^ + version: link:../../packages/pds + dd-trace: + specifier: 3.13.2 + version: 3.13.2 + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.19 + dev: true + + /@atproto/common@0.1.0: + resolution: {integrity: sha512-OB5tWE2R19jwiMIs2IjQieH5KTUuMb98XGCn9h3xuu6NanwjlmbCYMv08fMYwIp3UQ6jcq//84cDT3Bu6fJD+A==} + dependencies: + '@ipld/dag-cbor': 7.0.3 + multiformats: 9.9.0 + pino: 8.15.0 + zod: 3.21.4 + + /@atproto/crypto@0.1.0: + resolution: {integrity: sha512-9xgFEPtsCiJEPt9o3HtJT30IdFTGw5cQRSJVIy5CFhqBA4vDLcdXiRDLCjkzHEVbtNCsHUW6CrlfOgbeLPcmcg==} + dependencies: + '@noble/secp256k1': 1.7.1 + big-integer: 1.6.51 + multiformats: 9.9.0 + one-webcrypto: 1.0.3 + uint8arrays: 3.0.0 + + /@aws-crypto/crc32@2.0.0: + resolution: {integrity: sha512-TvE1r2CUueyXOuHdEigYjIZVesInd9KN+K/TFFNfkkxRThiNxO6i4ZqqAVMoEjAamZZ1AA8WXJkjCz7YShHPQA==} + dependencies: + '@aws-crypto/util': 2.0.2 + '@aws-sdk/types': 3.224.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/crc32c@2.0.0: + resolution: {integrity: sha512-vF0eMdMHx3O3MoOXUfBZry8Y4ZDtcuskjjKgJz8YfIDjLStxTZrYXk+kZqtl6A0uCmmiN/Eb/JbC/CndTV1MHg==} + dependencies: + '@aws-crypto/util': 2.0.2 + '@aws-sdk/types': 3.224.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/ie11-detection@2.0.2: + resolution: {integrity: sha512-5XDMQY98gMAf/WRTic5G++jfmS/VLM0rwpiOpaainKi4L0nqWMSB1SzsrEG5rjFZGYN6ZAefO+/Yta2dFM0kMw==} + dependencies: + tslib: 1.14.1 + dev: false + + /@aws-crypto/ie11-detection@3.0.0: + resolution: {integrity: sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==} + dependencies: + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha1-browser@2.0.0: + resolution: {integrity: sha512-3fIVRjPFY8EG5HWXR+ZJZMdWNRpwbxGzJ9IH9q93FpbgCH8u8GHRi46mZXp3cYD7gealmyqpm3ThZwLKJjWJhA==} + dependencies: + '@aws-crypto/ie11-detection': 2.0.2 + '@aws-crypto/supports-web-crypto': 2.0.2 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/util-locate-window': 3.310.0 + '@aws-sdk/util-utf8-browser': 3.188.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha256-browser@2.0.0: + resolution: {integrity: sha512-rYXOQ8BFOaqMEHJrLHul/25ckWH6GTJtdLSajhlqGMx0PmSueAuvboCuZCTqEKlxR8CQOwRarxYMZZSYlhRA1A==} + dependencies: + '@aws-crypto/ie11-detection': 2.0.2 + '@aws-crypto/sha256-js': 2.0.0 + '@aws-crypto/supports-web-crypto': 2.0.2 + '@aws-crypto/util': 2.0.2 + '@aws-sdk/types': 3.193.0 + '@aws-sdk/util-locate-window': 3.310.0 + '@aws-sdk/util-utf8-browser': 3.188.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha256-browser@3.0.0: + resolution: {integrity: sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==} + dependencies: + '@aws-crypto/ie11-detection': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-crypto/supports-web-crypto': 3.0.0 + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.257.0 + '@aws-sdk/util-locate-window': 3.310.0 + '@aws-sdk/util-utf8-browser': 3.259.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha256-js@2.0.0: + resolution: {integrity: sha512-VZY+mCY4Nmrs5WGfitmNqXzaE873fcIZDu54cbaDaaamsaTOP1DBImV9F4pICc3EHjQXujyE8jig+PFCaew9ig==} + dependencies: + '@aws-crypto/util': 2.0.2 + '@aws-sdk/types': 3.193.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha256-js@3.0.0: + resolution: {integrity: sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==} + dependencies: + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.257.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/supports-web-crypto@2.0.2: + resolution: {integrity: sha512-6mbSsLHwZ99CTOOswvCRP3C+VCWnzBf+1SnbWxzzJ9lR0mA0JnY2JEAhp8rqmTE0GPFy88rrM27ffgp62oErMQ==} + dependencies: + tslib: 1.14.1 + dev: false + + /@aws-crypto/supports-web-crypto@3.0.0: + resolution: {integrity: sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==} + dependencies: + tslib: 1.14.1 + dev: false + + /@aws-crypto/util@2.0.2: + resolution: {integrity: sha512-Lgu5v/0e/BcrZ5m/IWqzPUf3UYFTy/PpeED+uc9SWUR1iZQL8XXbGQg10UfllwwBryO3hFF5dizK+78aoXC1eA==} + dependencies: + '@aws-sdk/types': 3.193.0 + '@aws-sdk/util-utf8-browser': 3.188.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/util@3.0.0: + resolution: {integrity: sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==} + dependencies: + '@aws-sdk/types': 3.257.0 + '@aws-sdk/util-utf8-browser': 3.259.0 + tslib: 1.14.1 + dev: false + + /@aws-sdk/abort-controller@3.193.0: + resolution: {integrity: sha512-MYPBm5PWyKP+Tq37mKs5wDbyAyVMocF5iYmx738LYXBSj8A1V4LTFrvfd4U16BRC/sM0DYB9fBFJUQ9ISFRVYw==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/abort-controller@3.224.0: + resolution: {integrity: sha512-6DxaHnSDc2V5WiwtDaRwJJb2fkmDTyGr1svIM9H671aXIwe+q17mtpm5IooKL8bW5mLJoB1pT/5ntLkfxDQgSQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/abort-controller@3.257.0: + resolution: {integrity: sha512-ekWy391lOerS0ZECdhp/c+X7AToJIpfNrCPjuj3bKr+GMQYckGsYsdbm6AUD4sxBmfvuaQmVniSXWovaxwcFcQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/abort-controller@3.374.0: + resolution: {integrity: sha512-pO1pqFBdIF28ZvnJmg58Erj35RLzXsTrjvHghdc/xgtSvodFFCNrUsPg6AP3On8eiw9elpHoS4P8jMx1pHDXEw==} + engines: {node: '>=14.0.0'} + deprecated: This package has moved to @smithy/abort-controller + dependencies: + '@smithy/abort-controller': 1.1.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/chunked-blob-reader-native@3.208.0: + resolution: {integrity: sha512-JeOZ95PW+fJ6bbuqPySYqLqHk1n4+4ueEEraJsiUrPBV0S1ZtyvOGHcnGztKUjr2PYNaiexmpWuvUve9K12HRA==} + dependencies: + '@aws-sdk/util-base64': 3.208.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/chunked-blob-reader@3.188.0: + resolution: {integrity: sha512-zkPRFZZPL3eH+kH86LDYYXImiClA1/sW60zYOjse9Pgka+eDJlvBN6hcYxwDEKjcwATYiSRR1aVQHcfCinlGXg==} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/client-cloudfront@3.261.0: + resolution: {integrity: sha512-7JOpLfgYdQ+CDA3McsAmzcCO+rZj3wVicNTF7Kpl0JaZ0NB0NShifMb4OAGuh2RNh+OYV6k3mtjsXh9ZIQ08PQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/client-sts': 3.261.0 + '@aws-sdk/config-resolver': 3.259.0 + '@aws-sdk/credential-provider-node': 3.261.0 + '@aws-sdk/fetch-http-handler': 3.257.0 + '@aws-sdk/hash-node': 3.257.0 + '@aws-sdk/invalid-dependency': 3.257.0 + '@aws-sdk/middleware-content-length': 3.257.0 + '@aws-sdk/middleware-endpoint': 3.257.0 + '@aws-sdk/middleware-host-header': 3.257.0 + '@aws-sdk/middleware-logger': 3.257.0 + '@aws-sdk/middleware-recursion-detection': 3.257.0 + '@aws-sdk/middleware-retry': 3.259.0 + '@aws-sdk/middleware-serde': 3.257.0 + '@aws-sdk/middleware-signing': 3.257.0 + '@aws-sdk/middleware-stack': 3.257.0 + '@aws-sdk/middleware-user-agent': 3.257.0 + '@aws-sdk/node-config-provider': 3.259.0 + '@aws-sdk/node-http-handler': 3.257.0 + '@aws-sdk/protocol-http': 3.257.0 + '@aws-sdk/smithy-client': 3.261.0 + '@aws-sdk/types': 3.257.0 + '@aws-sdk/url-parser': 3.257.0 + '@aws-sdk/util-base64': 3.208.0 + '@aws-sdk/util-body-length-browser': 3.188.0 + '@aws-sdk/util-body-length-node': 3.208.0 + '@aws-sdk/util-defaults-mode-browser': 3.261.0 + '@aws-sdk/util-defaults-mode-node': 3.261.0 + '@aws-sdk/util-endpoints': 3.257.0 + '@aws-sdk/util-retry': 3.257.0 + '@aws-sdk/util-user-agent-browser': 3.257.0 + '@aws-sdk/util-user-agent-node': 3.259.0 + '@aws-sdk/util-utf8': 3.254.0 + '@aws-sdk/util-waiter': 3.257.0 + '@aws-sdk/xml-builder': 3.201.0 + fast-xml-parser: 4.0.11 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-kms@3.196.0: + resolution: {integrity: sha512-mR5jxfvHnv71FLd87PJ0KNgVXcZzNvKiI3i3JyLmukapnN5Kz2n0cG/jruo9d29zYQS60kfIPjdHddzOxNHH4A==} + engines: {node: '>=12.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 2.0.0 + '@aws-crypto/sha256-js': 2.0.0 + '@aws-sdk/client-sts': 3.196.0 + '@aws-sdk/config-resolver': 3.193.0 + '@aws-sdk/credential-provider-node': 3.196.0 + '@aws-sdk/fetch-http-handler': 3.193.0 + '@aws-sdk/hash-node': 3.193.0 + '@aws-sdk/invalid-dependency': 3.193.0 + '@aws-sdk/middleware-content-length': 3.193.0 + '@aws-sdk/middleware-endpoint': 3.193.0 + '@aws-sdk/middleware-host-header': 3.193.0 + '@aws-sdk/middleware-logger': 3.193.0 + '@aws-sdk/middleware-recursion-detection': 3.193.0 + '@aws-sdk/middleware-retry': 3.193.0 + '@aws-sdk/middleware-serde': 3.193.0 + '@aws-sdk/middleware-signing': 3.193.0 + '@aws-sdk/middleware-stack': 3.193.0 + '@aws-sdk/middleware-user-agent': 3.193.0 + '@aws-sdk/node-config-provider': 3.193.0 + '@aws-sdk/node-http-handler': 3.193.0 + '@aws-sdk/protocol-http': 3.193.0 + '@aws-sdk/smithy-client': 3.193.0 + '@aws-sdk/types': 3.193.0 + '@aws-sdk/url-parser': 3.193.0 + '@aws-sdk/util-base64-browser': 3.188.0 + '@aws-sdk/util-base64-node': 3.188.0 + '@aws-sdk/util-body-length-browser': 3.188.0 + '@aws-sdk/util-body-length-node': 3.188.0 + '@aws-sdk/util-defaults-mode-browser': 3.193.0 + '@aws-sdk/util-defaults-mode-node': 3.193.0 + '@aws-sdk/util-endpoints': 3.196.0 + '@aws-sdk/util-user-agent-browser': 3.193.0 + '@aws-sdk/util-user-agent-node': 3.193.0 + '@aws-sdk/util-utf8-browser': 3.188.0 + '@aws-sdk/util-utf8-node': 3.188.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-s3@3.224.0: + resolution: {integrity: sha512-CPU1sG4xr+fJ+OFpqz9Oum7cJwas0mA9YFvPLkgKLvNC2rhmmn0kbjwawtc6GUDu6xygeV8koBL2gz7OJHQ7fQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha1-browser': 2.0.0 + '@aws-crypto/sha256-browser': 2.0.0 + '@aws-crypto/sha256-js': 2.0.0 + '@aws-sdk/client-sts': 3.224.0 + '@aws-sdk/config-resolver': 3.224.0 + '@aws-sdk/credential-provider-node': 3.224.0 + '@aws-sdk/eventstream-serde-browser': 3.224.0 + '@aws-sdk/eventstream-serde-config-resolver': 3.224.0 + '@aws-sdk/eventstream-serde-node': 3.224.0 + '@aws-sdk/fetch-http-handler': 3.224.0 + '@aws-sdk/hash-blob-browser': 3.224.0 + '@aws-sdk/hash-node': 3.224.0 + '@aws-sdk/hash-stream-node': 3.224.0 + '@aws-sdk/invalid-dependency': 3.224.0 + '@aws-sdk/md5-js': 3.224.0 + '@aws-sdk/middleware-bucket-endpoint': 3.224.0 + '@aws-sdk/middleware-content-length': 3.224.0 + '@aws-sdk/middleware-endpoint': 3.224.0 + '@aws-sdk/middleware-expect-continue': 3.224.0 + '@aws-sdk/middleware-flexible-checksums': 3.224.0 + '@aws-sdk/middleware-host-header': 3.224.0 + '@aws-sdk/middleware-location-constraint': 3.224.0 + '@aws-sdk/middleware-logger': 3.224.0 + '@aws-sdk/middleware-recursion-detection': 3.224.0 + '@aws-sdk/middleware-retry': 3.224.0 + '@aws-sdk/middleware-sdk-s3': 3.224.0 + '@aws-sdk/middleware-serde': 3.224.0 + '@aws-sdk/middleware-signing': 3.224.0 + '@aws-sdk/middleware-ssec': 3.224.0 + '@aws-sdk/middleware-stack': 3.224.0 + '@aws-sdk/middleware-user-agent': 3.224.0 + '@aws-sdk/node-config-provider': 3.224.0 + '@aws-sdk/node-http-handler': 3.224.0 + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/signature-v4-multi-region': 3.224.0 + '@aws-sdk/smithy-client': 3.224.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/url-parser': 3.224.0 + '@aws-sdk/util-base64': 3.208.0 + '@aws-sdk/util-body-length-browser': 3.188.0 + '@aws-sdk/util-body-length-node': 3.208.0 + '@aws-sdk/util-defaults-mode-browser': 3.224.0 + '@aws-sdk/util-defaults-mode-node': 3.224.0 + '@aws-sdk/util-endpoints': 3.224.0 + '@aws-sdk/util-stream-browser': 3.224.0 + '@aws-sdk/util-stream-node': 3.224.0 + '@aws-sdk/util-user-agent-browser': 3.224.0 + '@aws-sdk/util-user-agent-node': 3.224.0 + '@aws-sdk/util-utf8-browser': 3.188.0 + '@aws-sdk/util-utf8-node': 3.208.0 + '@aws-sdk/util-waiter': 3.224.0 + '@aws-sdk/xml-builder': 3.201.0 + fast-xml-parser: 4.0.11 + tslib: 2.6.2 + transitivePeerDependencies: + - '@aws-sdk/signature-v4-crt' + - aws-crt + dev: false + + /@aws-sdk/client-sso-oidc@3.224.0: + resolution: {integrity: sha512-r7QAqinMvuZvGlfC4ltEBIq3gJ1AI4tTqEi8lG06+gDoiwnqTWii0+OrZJQiaeLc3PqDHwxmRpEmjFlr/f5TKg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 2.0.0 + '@aws-crypto/sha256-js': 2.0.0 + '@aws-sdk/config-resolver': 3.224.0 + '@aws-sdk/fetch-http-handler': 3.224.0 + '@aws-sdk/hash-node': 3.224.0 + '@aws-sdk/invalid-dependency': 3.224.0 + '@aws-sdk/middleware-content-length': 3.224.0 + '@aws-sdk/middleware-endpoint': 3.224.0 + '@aws-sdk/middleware-host-header': 3.224.0 + '@aws-sdk/middleware-logger': 3.224.0 + '@aws-sdk/middleware-recursion-detection': 3.224.0 + '@aws-sdk/middleware-retry': 3.224.0 + '@aws-sdk/middleware-serde': 3.224.0 + '@aws-sdk/middleware-stack': 3.224.0 + '@aws-sdk/middleware-user-agent': 3.224.0 + '@aws-sdk/node-config-provider': 3.224.0 + '@aws-sdk/node-http-handler': 3.224.0 + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/smithy-client': 3.224.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/url-parser': 3.224.0 + '@aws-sdk/util-base64': 3.208.0 + '@aws-sdk/util-body-length-browser': 3.188.0 + '@aws-sdk/util-body-length-node': 3.208.0 + '@aws-sdk/util-defaults-mode-browser': 3.224.0 + '@aws-sdk/util-defaults-mode-node': 3.224.0 + '@aws-sdk/util-endpoints': 3.224.0 + '@aws-sdk/util-user-agent-browser': 3.224.0 + '@aws-sdk/util-user-agent-node': 3.224.0 + '@aws-sdk/util-utf8-browser': 3.188.0 + '@aws-sdk/util-utf8-node': 3.208.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sso-oidc@3.261.0: + resolution: {integrity: sha512-ItgRT/BThv2UxEeGJ5/GCF6JY1Rzk39IcDIPZAfBA8HbYcznXGDsBTRf45MErS+uollwNFX0T/WNlTbmjEDE7g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/config-resolver': 3.259.0 + '@aws-sdk/fetch-http-handler': 3.257.0 + '@aws-sdk/hash-node': 3.257.0 + '@aws-sdk/invalid-dependency': 3.257.0 + '@aws-sdk/middleware-content-length': 3.257.0 + '@aws-sdk/middleware-endpoint': 3.257.0 + '@aws-sdk/middleware-host-header': 3.257.0 + '@aws-sdk/middleware-logger': 3.257.0 + '@aws-sdk/middleware-recursion-detection': 3.257.0 + '@aws-sdk/middleware-retry': 3.259.0 + '@aws-sdk/middleware-serde': 3.257.0 + '@aws-sdk/middleware-stack': 3.257.0 + '@aws-sdk/middleware-user-agent': 3.257.0 + '@aws-sdk/node-config-provider': 3.259.0 + '@aws-sdk/node-http-handler': 3.257.0 + '@aws-sdk/protocol-http': 3.257.0 + '@aws-sdk/smithy-client': 3.261.0 + '@aws-sdk/types': 3.257.0 + '@aws-sdk/url-parser': 3.257.0 + '@aws-sdk/util-base64': 3.208.0 + '@aws-sdk/util-body-length-browser': 3.188.0 + '@aws-sdk/util-body-length-node': 3.208.0 + '@aws-sdk/util-defaults-mode-browser': 3.261.0 + '@aws-sdk/util-defaults-mode-node': 3.261.0 + '@aws-sdk/util-endpoints': 3.257.0 + '@aws-sdk/util-retry': 3.257.0 + '@aws-sdk/util-user-agent-browser': 3.257.0 + '@aws-sdk/util-user-agent-node': 3.259.0 + '@aws-sdk/util-utf8': 3.254.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sso@3.196.0: + resolution: {integrity: sha512-u+UnxrVHLjLDdfCZft1AuyIhyv+77/inCHR4LcKsGASRA+jAg3z+OY+B7Q9hWHNcVt5ECMw7rxe4jA9BLf42sw==} + engines: {node: '>=12.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 2.0.0 + '@aws-crypto/sha256-js': 2.0.0 + '@aws-sdk/config-resolver': 3.193.0 + '@aws-sdk/fetch-http-handler': 3.193.0 + '@aws-sdk/hash-node': 3.193.0 + '@aws-sdk/invalid-dependency': 3.193.0 + '@aws-sdk/middleware-content-length': 3.193.0 + '@aws-sdk/middleware-endpoint': 3.193.0 + '@aws-sdk/middleware-host-header': 3.193.0 + '@aws-sdk/middleware-logger': 3.193.0 + '@aws-sdk/middleware-recursion-detection': 3.193.0 + '@aws-sdk/middleware-retry': 3.193.0 + '@aws-sdk/middleware-serde': 3.193.0 + '@aws-sdk/middleware-stack': 3.193.0 + '@aws-sdk/middleware-user-agent': 3.193.0 + '@aws-sdk/node-config-provider': 3.193.0 + '@aws-sdk/node-http-handler': 3.193.0 + '@aws-sdk/protocol-http': 3.193.0 + '@aws-sdk/smithy-client': 3.193.0 + '@aws-sdk/types': 3.193.0 + '@aws-sdk/url-parser': 3.193.0 + '@aws-sdk/util-base64-browser': 3.188.0 + '@aws-sdk/util-base64-node': 3.188.0 + '@aws-sdk/util-body-length-browser': 3.188.0 + '@aws-sdk/util-body-length-node': 3.188.0 + '@aws-sdk/util-defaults-mode-browser': 3.193.0 + '@aws-sdk/util-defaults-mode-node': 3.193.0 + '@aws-sdk/util-endpoints': 3.196.0 + '@aws-sdk/util-user-agent-browser': 3.193.0 + '@aws-sdk/util-user-agent-node': 3.193.0 + '@aws-sdk/util-utf8-browser': 3.188.0 + '@aws-sdk/util-utf8-node': 3.188.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sso@3.224.0: + resolution: {integrity: sha512-ZfqjGGBhv+sKxYN9FHbepaL+ucFbAFndvNdalGj4mZsv5AqxgemkFoRofNJk4nu79JVf5cdrj7zL+BDW3KwEGg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 2.0.0 + '@aws-crypto/sha256-js': 2.0.0 + '@aws-sdk/config-resolver': 3.224.0 + '@aws-sdk/fetch-http-handler': 3.224.0 + '@aws-sdk/hash-node': 3.224.0 + '@aws-sdk/invalid-dependency': 3.224.0 + '@aws-sdk/middleware-content-length': 3.224.0 + '@aws-sdk/middleware-endpoint': 3.224.0 + '@aws-sdk/middleware-host-header': 3.224.0 + '@aws-sdk/middleware-logger': 3.224.0 + '@aws-sdk/middleware-recursion-detection': 3.224.0 + '@aws-sdk/middleware-retry': 3.224.0 + '@aws-sdk/middleware-serde': 3.224.0 + '@aws-sdk/middleware-stack': 3.224.0 + '@aws-sdk/middleware-user-agent': 3.224.0 + '@aws-sdk/node-config-provider': 3.224.0 + '@aws-sdk/node-http-handler': 3.224.0 + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/smithy-client': 3.224.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/url-parser': 3.224.0 + '@aws-sdk/util-base64': 3.208.0 + '@aws-sdk/util-body-length-browser': 3.188.0 + '@aws-sdk/util-body-length-node': 3.208.0 + '@aws-sdk/util-defaults-mode-browser': 3.224.0 + '@aws-sdk/util-defaults-mode-node': 3.224.0 + '@aws-sdk/util-endpoints': 3.224.0 + '@aws-sdk/util-user-agent-browser': 3.224.0 + '@aws-sdk/util-user-agent-node': 3.224.0 + '@aws-sdk/util-utf8-browser': 3.188.0 + '@aws-sdk/util-utf8-node': 3.208.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sso@3.261.0: + resolution: {integrity: sha512-tq5hu1WXa9BKsCH9zOBOykyiaoZQvaFHKdOamw5SZ69niyO3AG4xR1TkLqXj/9mDYMLgAIVObKZDGWtBLFTdiQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/config-resolver': 3.259.0 + '@aws-sdk/fetch-http-handler': 3.257.0 + '@aws-sdk/hash-node': 3.257.0 + '@aws-sdk/invalid-dependency': 3.257.0 + '@aws-sdk/middleware-content-length': 3.257.0 + '@aws-sdk/middleware-endpoint': 3.257.0 + '@aws-sdk/middleware-host-header': 3.257.0 + '@aws-sdk/middleware-logger': 3.257.0 + '@aws-sdk/middleware-recursion-detection': 3.257.0 + '@aws-sdk/middleware-retry': 3.259.0 + '@aws-sdk/middleware-serde': 3.257.0 + '@aws-sdk/middleware-stack': 3.257.0 + '@aws-sdk/middleware-user-agent': 3.257.0 + '@aws-sdk/node-config-provider': 3.259.0 + '@aws-sdk/node-http-handler': 3.257.0 + '@aws-sdk/protocol-http': 3.257.0 + '@aws-sdk/smithy-client': 3.261.0 + '@aws-sdk/types': 3.257.0 + '@aws-sdk/url-parser': 3.257.0 + '@aws-sdk/util-base64': 3.208.0 + '@aws-sdk/util-body-length-browser': 3.188.0 + '@aws-sdk/util-body-length-node': 3.208.0 + '@aws-sdk/util-defaults-mode-browser': 3.261.0 + '@aws-sdk/util-defaults-mode-node': 3.261.0 + '@aws-sdk/util-endpoints': 3.257.0 + '@aws-sdk/util-retry': 3.257.0 + '@aws-sdk/util-user-agent-browser': 3.257.0 + '@aws-sdk/util-user-agent-node': 3.259.0 + '@aws-sdk/util-utf8': 3.254.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sts@3.196.0: + resolution: {integrity: sha512-ChzK8606CugwnRLm7iwerXzeMqOsjGLe3j1j1HtQShzXZu4/ysQ3mUBBPAt2Lltx+1ep8MoI9vaQVyfw5h35ww==} + engines: {node: '>=12.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 2.0.0 + '@aws-crypto/sha256-js': 2.0.0 + '@aws-sdk/config-resolver': 3.193.0 + '@aws-sdk/credential-provider-node': 3.196.0 + '@aws-sdk/fetch-http-handler': 3.193.0 + '@aws-sdk/hash-node': 3.193.0 + '@aws-sdk/invalid-dependency': 3.193.0 + '@aws-sdk/middleware-content-length': 3.193.0 + '@aws-sdk/middleware-endpoint': 3.193.0 + '@aws-sdk/middleware-host-header': 3.193.0 + '@aws-sdk/middleware-logger': 3.193.0 + '@aws-sdk/middleware-recursion-detection': 3.193.0 + '@aws-sdk/middleware-retry': 3.193.0 + '@aws-sdk/middleware-sdk-sts': 3.193.0 + '@aws-sdk/middleware-serde': 3.193.0 + '@aws-sdk/middleware-signing': 3.193.0 + '@aws-sdk/middleware-stack': 3.193.0 + '@aws-sdk/middleware-user-agent': 3.193.0 + '@aws-sdk/node-config-provider': 3.193.0 + '@aws-sdk/node-http-handler': 3.193.0 + '@aws-sdk/protocol-http': 3.193.0 + '@aws-sdk/smithy-client': 3.193.0 + '@aws-sdk/types': 3.193.0 + '@aws-sdk/url-parser': 3.193.0 + '@aws-sdk/util-base64-browser': 3.188.0 + '@aws-sdk/util-base64-node': 3.188.0 + '@aws-sdk/util-body-length-browser': 3.188.0 + '@aws-sdk/util-body-length-node': 3.188.0 + '@aws-sdk/util-defaults-mode-browser': 3.193.0 + '@aws-sdk/util-defaults-mode-node': 3.193.0 + '@aws-sdk/util-endpoints': 3.196.0 + '@aws-sdk/util-user-agent-browser': 3.193.0 + '@aws-sdk/util-user-agent-node': 3.193.0 + '@aws-sdk/util-utf8-browser': 3.188.0 + '@aws-sdk/util-utf8-node': 3.188.0 + fast-xml-parser: 4.0.11 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sts@3.224.0: + resolution: {integrity: sha512-ao3jyjwk2fozk1d4PtrNf0BNsucPWAbALv8CCsPTC3r9g2Lg/TOi3pxmsfd69ddw89XSyP6zZATEHaWO+tk0CQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 2.0.0 + '@aws-crypto/sha256-js': 2.0.0 + '@aws-sdk/config-resolver': 3.224.0 + '@aws-sdk/credential-provider-node': 3.224.0 + '@aws-sdk/fetch-http-handler': 3.224.0 + '@aws-sdk/hash-node': 3.224.0 + '@aws-sdk/invalid-dependency': 3.224.0 + '@aws-sdk/middleware-content-length': 3.224.0 + '@aws-sdk/middleware-endpoint': 3.224.0 + '@aws-sdk/middleware-host-header': 3.224.0 + '@aws-sdk/middleware-logger': 3.224.0 + '@aws-sdk/middleware-recursion-detection': 3.224.0 + '@aws-sdk/middleware-retry': 3.224.0 + '@aws-sdk/middleware-sdk-sts': 3.224.0 + '@aws-sdk/middleware-serde': 3.224.0 + '@aws-sdk/middleware-signing': 3.224.0 + '@aws-sdk/middleware-stack': 3.224.0 + '@aws-sdk/middleware-user-agent': 3.224.0 + '@aws-sdk/node-config-provider': 3.224.0 + '@aws-sdk/node-http-handler': 3.224.0 + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/smithy-client': 3.224.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/url-parser': 3.224.0 + '@aws-sdk/util-base64': 3.208.0 + '@aws-sdk/util-body-length-browser': 3.188.0 + '@aws-sdk/util-body-length-node': 3.208.0 + '@aws-sdk/util-defaults-mode-browser': 3.224.0 + '@aws-sdk/util-defaults-mode-node': 3.224.0 + '@aws-sdk/util-endpoints': 3.224.0 + '@aws-sdk/util-user-agent-browser': 3.224.0 + '@aws-sdk/util-user-agent-node': 3.224.0 + '@aws-sdk/util-utf8-browser': 3.188.0 + '@aws-sdk/util-utf8-node': 3.208.0 + fast-xml-parser: 4.0.11 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sts@3.261.0: + resolution: {integrity: sha512-jnCKBjuHEMgwCmR9bXDVpl/WzpUQyU9DL3Mk65XYyZwRxgHSaw5D90zRouoZMUneNA2OnKZQnjk6oyL47mb7oA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/config-resolver': 3.259.0 + '@aws-sdk/credential-provider-node': 3.261.0 + '@aws-sdk/fetch-http-handler': 3.257.0 + '@aws-sdk/hash-node': 3.257.0 + '@aws-sdk/invalid-dependency': 3.257.0 + '@aws-sdk/middleware-content-length': 3.257.0 + '@aws-sdk/middleware-endpoint': 3.257.0 + '@aws-sdk/middleware-host-header': 3.257.0 + '@aws-sdk/middleware-logger': 3.257.0 + '@aws-sdk/middleware-recursion-detection': 3.257.0 + '@aws-sdk/middleware-retry': 3.259.0 + '@aws-sdk/middleware-sdk-sts': 3.257.0 + '@aws-sdk/middleware-serde': 3.257.0 + '@aws-sdk/middleware-signing': 3.257.0 + '@aws-sdk/middleware-stack': 3.257.0 + '@aws-sdk/middleware-user-agent': 3.257.0 + '@aws-sdk/node-config-provider': 3.259.0 + '@aws-sdk/node-http-handler': 3.257.0 + '@aws-sdk/protocol-http': 3.257.0 + '@aws-sdk/smithy-client': 3.261.0 + '@aws-sdk/types': 3.257.0 + '@aws-sdk/url-parser': 3.257.0 + '@aws-sdk/util-base64': 3.208.0 + '@aws-sdk/util-body-length-browser': 3.188.0 + '@aws-sdk/util-body-length-node': 3.208.0 + '@aws-sdk/util-defaults-mode-browser': 3.261.0 + '@aws-sdk/util-defaults-mode-node': 3.261.0 + '@aws-sdk/util-endpoints': 3.257.0 + '@aws-sdk/util-retry': 3.257.0 + '@aws-sdk/util-user-agent-browser': 3.257.0 + '@aws-sdk/util-user-agent-node': 3.259.0 + '@aws-sdk/util-utf8': 3.254.0 + fast-xml-parser: 4.0.11 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/config-resolver@3.193.0: + resolution: {integrity: sha512-HIjuv2A1glgkXy9g/A8bfsiz3jTFaRbwGZheoHFZod6iEQQEbbeAsBe3u2AZyzOrVLgs8lOvBtgU8XKSJWjDkw==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/signature-v4': 3.193.0 + '@aws-sdk/types': 3.193.0 + '@aws-sdk/util-config-provider': 3.188.0 + '@aws-sdk/util-middleware': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/config-resolver@3.224.0: + resolution: {integrity: sha512-jS53QvF2jdv7d6cpPUH6N85i1WNHik1eGvxqSndsNbLf0keEGXYyN4pBLNB0xK1nk0ZG+8slRsXgWvWTCcFYKA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/signature-v4': 3.224.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/util-config-provider': 3.208.0 + '@aws-sdk/util-middleware': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/config-resolver@3.259.0: + resolution: {integrity: sha512-gViMRsc4Ye6+nzJ0OYTZIT8m4glIAdtugN2Sr/t6P2iJW5X0bSL/EcbcHBgsve1lHjeGPeyzVkT7UnyGOZ5Z/A==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/signature-v4': 3.257.0 + '@aws-sdk/types': 3.257.0 + '@aws-sdk/util-config-provider': 3.208.0 + '@aws-sdk/util-middleware': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/credential-provider-env@3.193.0: + resolution: {integrity: sha512-pRqZoIaqCdWB4JJdR6DqDn3u+CwKJchwiCPnRtChwC8KXCMkT4njq9J1bWG3imYeTxP/G06O1PDONEuD4pPtNQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/credential-provider-env@3.224.0: + resolution: {integrity: sha512-WUicVivCne9Ela2Nuufohy8+UV/W6GwanlpK9trJqrqHt2/zqdNYHqZbWL0zDNO8dvFN3+MC2a8boYPyR+cFRg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/credential-provider-env@3.257.0: + resolution: {integrity: sha512-GsmBi5Di6hk1JAi1iB6/LCY8o+GmlCvJoB7wuoVmXI3VxRVwptUVjuj8EtJbIrVGrF9dSuIRPCzUoSuzEzYGlg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/credential-provider-imds@3.193.0: + resolution: {integrity: sha512-jC7uT7uVpO/iitz49toHMGFKXQ2igWQQG2SKirREqDRaz5HSXwEP1V3rcOlNNyGIBPMggDjZnxYgJHqBXSq9Ag==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/node-config-provider': 3.193.0 + '@aws-sdk/property-provider': 3.193.0 + '@aws-sdk/types': 3.193.0 + '@aws-sdk/url-parser': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/credential-provider-imds@3.224.0: + resolution: {integrity: sha512-n7uVR5Z9EUfVbg0gSNrJvu1g0cM/HqhRt+kaRJBGNf4q1tEbnCukKj+qUZbT1qdbDTyu9NTRphMvuIyN3RBDtQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/node-config-provider': 3.224.0 + '@aws-sdk/property-provider': 3.224.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/url-parser': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/credential-provider-imds@3.259.0: + resolution: {integrity: sha512-yCxoYWZAaDrCUEWxRfrpB0Mp1cFgJEMYW8T6GIb/+DQ5QLpZmorgaVD/j90QXupqFrR5tlxwuskBIkdD2E9YNg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/node-config-provider': 3.259.0 + '@aws-sdk/property-provider': 3.257.0 + '@aws-sdk/types': 3.257.0 + '@aws-sdk/url-parser': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/credential-provider-ini@3.196.0: + resolution: {integrity: sha512-3lL+YLBQ9KwQxG4AdRm4u2cvBNZeBmS/i3BWnCPomg96lNGPMrTEloVaVEpnrzOff6sgFxRtjkbLkVxmdipIrw==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.193.0 + '@aws-sdk/credential-provider-imds': 3.193.0 + '@aws-sdk/credential-provider-sso': 3.196.0 + '@aws-sdk/credential-provider-web-identity': 3.193.0 + '@aws-sdk/property-provider': 3.193.0 + '@aws-sdk/shared-ini-file-loader': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-ini@3.224.0: + resolution: {integrity: sha512-YaAHoHJVspqy5f8C6EXBifMfodKXl88IHuL6eBComigTPR3s1Ed1+3AJdjA1X7SjAHfrYna/WvZEH3e8NCSzFA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.224.0 + '@aws-sdk/credential-provider-imds': 3.224.0 + '@aws-sdk/credential-provider-sso': 3.224.0 + '@aws-sdk/credential-provider-web-identity': 3.224.0 + '@aws-sdk/property-provider': 3.224.0 + '@aws-sdk/shared-ini-file-loader': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-ini@3.261.0: + resolution: {integrity: sha512-638jTnvFbGO0G0So+FijdC1vjn/dhw3l8nJwLq9PYOBJUKhjXDR/fpOhZkUJ+Zwfuqp9SlDDo/yfFa6j2L+F1g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.257.0 + '@aws-sdk/credential-provider-imds': 3.259.0 + '@aws-sdk/credential-provider-process': 3.257.0 + '@aws-sdk/credential-provider-sso': 3.261.0 + '@aws-sdk/credential-provider-web-identity': 3.257.0 + '@aws-sdk/property-provider': 3.257.0 + '@aws-sdk/shared-ini-file-loader': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-node@3.196.0: + resolution: {integrity: sha512-PGY7pkmqgfEwTHsuUH6fGrXWri93jqKkMbhq/QJafMGtsVupfvXvE37Rl+qgjsZjRfROrEaeLw2DGrPPmVh2cg==} + engines: {node: '>=12.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.193.0 + '@aws-sdk/credential-provider-imds': 3.193.0 + '@aws-sdk/credential-provider-ini': 3.196.0 + '@aws-sdk/credential-provider-process': 3.193.0 + '@aws-sdk/credential-provider-sso': 3.196.0 + '@aws-sdk/credential-provider-web-identity': 3.193.0 + '@aws-sdk/property-provider': 3.193.0 + '@aws-sdk/shared-ini-file-loader': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-node@3.224.0: + resolution: {integrity: sha512-n/gijJAA3uVFl1b3+hp2E3lPaiajsPLHqH+mMxNxPkGo39HV1v9RAyOVW4Y3AH1QcT7sURevjGoF2Eemcro88g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.224.0 + '@aws-sdk/credential-provider-imds': 3.224.0 + '@aws-sdk/credential-provider-ini': 3.224.0 + '@aws-sdk/credential-provider-process': 3.224.0 + '@aws-sdk/credential-provider-sso': 3.224.0 + '@aws-sdk/credential-provider-web-identity': 3.224.0 + '@aws-sdk/property-provider': 3.224.0 + '@aws-sdk/shared-ini-file-loader': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-node@3.261.0: + resolution: {integrity: sha512-7T25a7jbHsXPe7XvIekzhR50b7PTlISKqHdE8LNVUSzFQbSjVXulFk3vyQVIhmt5HKNkSBcMPDr6hKrSl7OLBw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.257.0 + '@aws-sdk/credential-provider-imds': 3.259.0 + '@aws-sdk/credential-provider-ini': 3.261.0 + '@aws-sdk/credential-provider-process': 3.257.0 + '@aws-sdk/credential-provider-sso': 3.261.0 + '@aws-sdk/credential-provider-web-identity': 3.257.0 + '@aws-sdk/property-provider': 3.257.0 + '@aws-sdk/shared-ini-file-loader': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-process@3.193.0: + resolution: {integrity: sha512-zpXxtQzQqkaUuFqmHW9dSkh9p/1k+XNKlwEkG8FTwAJNUWmy2ZMJv+8NTVn4s4vaRu7xJ1er9chspYr7mvxHlA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.193.0 + '@aws-sdk/shared-ini-file-loader': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/credential-provider-process@3.224.0: + resolution: {integrity: sha512-0nc8vGmv6vDfFlVyKREwAa4namfuGqKg3TTM0nW2vE10fpDXZM/DGVAs5HInX+27QQNLVVh3/OHHgti9wMkYkw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.224.0 + '@aws-sdk/shared-ini-file-loader': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/credential-provider-process@3.257.0: + resolution: {integrity: sha512-xK8uYeNXaclaBCGrLi4z2pxPRngqLf5BM5jg2fn57zqvlL9V5gJF972FehrVBL0bfp1/laG0ZJtD2K2sapyWAw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.257.0 + '@aws-sdk/shared-ini-file-loader': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/credential-provider-sso@3.196.0: + resolution: {integrity: sha512-hJV4LDVfvPfj5zC0ysHx3zkwwJOyF+BaMGaMzaScrHyijv5e3qZzdoBLbOQFmrqVnt7DjCU02NvRSS8amLpmSw==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/client-sso': 3.196.0 + '@aws-sdk/property-provider': 3.193.0 + '@aws-sdk/shared-ini-file-loader': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-sso@3.224.0: + resolution: {integrity: sha512-Qx5w8MCGAwT5cqimA3ZgtY1jSrC7QGPzZfNflY75PWQIaYgjUNNqdAW0jipr4M/dgVjvo1j/Ek+atNf/niTOsQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/client-sso': 3.224.0 + '@aws-sdk/property-provider': 3.224.0 + '@aws-sdk/shared-ini-file-loader': 3.224.0 + '@aws-sdk/token-providers': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-sso@3.261.0: + resolution: {integrity: sha512-Ofj7m85/RuxcZMtghhD+U2GGszrU5tB2kxXcnkcHCudOER6bcOOEXnSfmdZnIv4xG+vma3VFwiWk2JkQo5zB5w==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/client-sso': 3.261.0 + '@aws-sdk/property-provider': 3.257.0 + '@aws-sdk/shared-ini-file-loader': 3.257.0 + '@aws-sdk/token-providers': 3.261.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-web-identity@3.193.0: + resolution: {integrity: sha512-MIQY9KwLCBnRyIt7an4EtMrFQZz2HC1E8vQDdKVzmeQBBePhW61fnX9XDP9bfc3Ypg1NggLG00KBPEC88twLFg==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/credential-provider-web-identity@3.224.0: + resolution: {integrity: sha512-Z/xRFTm9pBVyuIAkYohisb3KPJowPVng7ZuZiblU0PaESoJBTkhAFOblpPv/ZWwb6fT85ANUKrvl4858zLpk/Q==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/credential-provider-web-identity@3.257.0: + resolution: {integrity: sha512-Cm0uvRv4JuIbD0Kp3W0J/vwjADIyCx8HoZi5yg+QIi5nilocuTQ3ajvLeuPVSvFvdy+yaxSc5FxNXquWt7Mngw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/eventstream-codec@3.224.0: + resolution: {integrity: sha512-p8DePCwvgrrlYK7r3euI5aX/VVxrCl+DClHy0TV6/Eq8WCgWqYfZ5TSl5kbrxIc4U7pDlNIBkTiQMIl/ilEiQg==} + dependencies: + '@aws-crypto/crc32': 2.0.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/util-hex-encoding': 3.201.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/eventstream-serde-browser@3.224.0: + resolution: {integrity: sha512-QeyGmKipZsbVkezI5OKe0Xad7u1JPkZWNm1m7uqjd9vTK3A+/fw7eNxOWYVdSKs/kHyAWr9PG+fASBtr3gesPA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/eventstream-serde-universal': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/eventstream-serde-config-resolver@3.224.0: + resolution: {integrity: sha512-zl8YUa+JZV9Dj304pc2HovMuUsz3qzo8HHj+FjIHxVsNfFL4U/NX/eDhkiWNUwVMlQIWvjksoJZ75kIzDyWGKQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/eventstream-serde-node@3.224.0: + resolution: {integrity: sha512-o0PXQwyyqeBk+kkn9wIPVIdzwp28EmRt2UqEH/UO6XzpmZTghuY4ZWkQTx9n+vMZ0e/EbqIlh2BPAhELwYzMug==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/eventstream-serde-universal': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/eventstream-serde-universal@3.224.0: + resolution: {integrity: sha512-sI9WKnaKfpVamLCESHDOg8SkMtkjjYX3awny5PJC3/Jx9zOFN9AnvGtnIJrOGFxs5kBmQNj7c4sKCAPiTCcITw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/eventstream-codec': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/fetch-http-handler@3.193.0: + resolution: {integrity: sha512-UhIS2LtCK9hqBzYVon6BI8WebJW1KC0GGIL/Gse5bqzU9iAGgFLAe66qg9k+/h3Jjc5LNAYzqXNVizMwn7689Q==} + dependencies: + '@aws-sdk/protocol-http': 3.193.0 + '@aws-sdk/querystring-builder': 3.193.0 + '@aws-sdk/types': 3.193.0 + '@aws-sdk/util-base64-browser': 3.188.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/fetch-http-handler@3.224.0: + resolution: {integrity: sha512-IO1Je6ZM0fwT5YYPwQwwXcD4LlsYmP52pwit8AAI4ppz6AkSfs0747uDK0DYnqls7sevBQzUSqBSt6XjcMKjYQ==} + dependencies: + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/querystring-builder': 3.224.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/util-base64': 3.208.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/fetch-http-handler@3.257.0: + resolution: {integrity: sha512-zOF+RzQ+wfF7tq7tGUdPcqUTh3+k2f8KCVJE07A8kCopVq4nBu4NH6Eq29Tjpwdya3YlKvE+kFssuQRRRRex+Q==} + dependencies: + '@aws-sdk/protocol-http': 3.257.0 + '@aws-sdk/querystring-builder': 3.257.0 + '@aws-sdk/types': 3.257.0 + '@aws-sdk/util-base64': 3.208.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/hash-blob-browser@3.224.0: + resolution: {integrity: sha512-nUBRZzxbq6mU8FIK6OizC5jIeRkVn5tB2ZYxPd7P2IDhh1OVod6gXkq9EKTf3kecNvYgWUVHcOezZF1qLMDLHg==} + dependencies: + '@aws-sdk/chunked-blob-reader': 3.188.0 + '@aws-sdk/chunked-blob-reader-native': 3.208.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/hash-node@3.193.0: + resolution: {integrity: sha512-O2SLPVBjrCUo+4ouAdRUoHBYsyurO9LcjNZNYD7YQOotBTbVFA3cx7kTZu+K4B6kX7FDaGbqbE1C/T1/eg/r+w==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.193.0 + '@aws-sdk/util-buffer-from': 3.188.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/hash-node@3.224.0: + resolution: {integrity: sha512-y7TXMDOSy5E2VZPvmsvRfyXkcQWcjTLFTd85yc70AAeFZiffff1nvZifQSzD78bW6ELJsWHXA2O8yxdBURyoBg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.224.0 + '@aws-sdk/util-buffer-from': 3.208.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/hash-node@3.257.0: + resolution: {integrity: sha512-W/USUuea5Ep3OJ2U7Ve8/5KN1YsDun2WzOFUxc1PyxXP5pW6OgC15/op0e+bmWPG851clvp5S8ZuroUr3aKi3Q==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.257.0 + '@aws-sdk/util-buffer-from': 3.208.0 + '@aws-sdk/util-utf8': 3.254.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/hash-stream-node@3.224.0: + resolution: {integrity: sha512-5RDwzB2C4Zjn4M2kZYntkc2LJdqe8CH9xmudu3ZYESkZToN5Rd3JyqobW9KPbm//R43VR4ml2qUhYHFzK6jvgg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/invalid-dependency@3.193.0: + resolution: {integrity: sha512-54DCknekLwJAI1os76XJ8XCzfAH7BGkBGtlWk5WCNkZTfj3rf5RUiXz4uoKUMWE1rZmyMDoDDS1PBo+yTVKW5w==} + dependencies: + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/invalid-dependency@3.224.0: + resolution: {integrity: sha512-6huV8LBYQYx84uMhQ2SS7nqEkhTkAufwhKceXnysrcrLDuUmyth09Y7fcFblFIDTr4wTgSI0mf6DKVF4nqYCwQ==} + dependencies: + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/invalid-dependency@3.257.0: + resolution: {integrity: sha512-T68SAPRNMEhpke0wlxURgogL7q0B8dfqZsSeS20BVR/lksJxLse9+pbmCDxiu1RrXoEIsEwl5rbLN+Hw8BFFYw==} + dependencies: + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/is-array-buffer@3.188.0: + resolution: {integrity: sha512-n69N4zJZCNd87Rf4NzufPzhactUeM877Y0Tp/F3KiHqGeTnVjYUa4Lv1vLBjqtfjYb2HWT3NKlYn5yzrhaEwiQ==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/is-array-buffer@3.201.0: + resolution: {integrity: sha512-UPez5qLh3dNgt0DYnPD/q0mVJY84rA17QE26hVNOW3fAji8W2wrwrxdacWOxyXvlxWsVRcKmr+lay1MDqpAMfg==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/lib-storage@3.226.0(@aws-sdk/abort-controller@3.374.0)(@aws-sdk/client-s3@3.224.0): + resolution: {integrity: sha512-pTPQlZqYhonkaSpdD582fKKfUtQv+80vcyJdmAelUC4hZIyT98XT0wzZLp5N8etAFAgVj7Lxh59qxPB4Qz8MCw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@aws-sdk/abort-controller': ^3.0.0 + '@aws-sdk/client-s3': ^3.0.0 + dependencies: + '@aws-sdk/abort-controller': 3.374.0 + '@aws-sdk/client-s3': 3.224.0 + '@aws-sdk/middleware-endpoint': 3.226.0 + '@aws-sdk/smithy-client': 3.226.0 + buffer: 5.6.0 + events: 3.3.0 + stream-browserify: 3.0.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/md5-js@3.224.0: + resolution: {integrity: sha512-DT9hKzBYJUcPvGxTXwoug5Ac4zJ7q5pwOVF/PFCsN3TiXHHfDAIA0/GJjA6pZwPEi/qVy0iNhGKQK8/0i5JeWw==} + dependencies: + '@aws-sdk/types': 3.224.0 + '@aws-sdk/util-utf8-browser': 3.188.0 + '@aws-sdk/util-utf8-node': 3.208.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-bucket-endpoint@3.224.0: + resolution: {integrity: sha512-cAmrSmVjBCENM9ojUBRhIsuQ2mPH4WxnqE5wxloHdP8BD7usNE/dMtGMhot3Dnf8WZEFpTMfhtrZrmSTCaANTQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/util-arn-parser': 3.208.0 + '@aws-sdk/util-config-provider': 3.208.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-content-length@3.193.0: + resolution: {integrity: sha512-em0Sqo7O7DFOcVXU460pbcYuIjblDTZqK2YE62nQ0T+5Nbj+MSjuoite+rRRdRww9VqBkUROGKON45bUNjogtQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-content-length@3.224.0: + resolution: {integrity: sha512-L9b84b7X/BH+sFZaXg5hQQv0TRqZIGuOIiWJ8CkYeju7OQV03DzbCoNCAgZdI28SSevfrrVK/hwjEQrv+A6x1Q==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-content-length@3.257.0: + resolution: {integrity: sha512-yiawbV2azm6QnMY1L2ypG8PDRdjOcEIvFmT0T7y0F49rfbKJOu21j1ONAoCkLrINK6kMqcD5JSQLVCoURxiTxQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-endpoint@3.193.0: + resolution: {integrity: sha512-Inbpt7jcHGvzF7UOJOCxx9wih0+eAQYERikokidWJa7M405EJpVYq1mGbeOcQUPANU3uWF1AObmUUFhbkriHQw==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/middleware-serde': 3.193.0 + '@aws-sdk/protocol-http': 3.193.0 + '@aws-sdk/signature-v4': 3.193.0 + '@aws-sdk/types': 3.193.0 + '@aws-sdk/url-parser': 3.193.0 + '@aws-sdk/util-config-provider': 3.188.0 + '@aws-sdk/util-middleware': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-endpoint@3.224.0: + resolution: {integrity: sha512-Y+FkQmRyhQUX1E1tviodFwTrfAVjgteoALkFgIb7bxT7fmyQ/AQvdAytkDqIApTgkR61niNDSsAu7lHekDxQgg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-serde': 3.224.0 + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/signature-v4': 3.224.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/url-parser': 3.224.0 + '@aws-sdk/util-config-provider': 3.208.0 + '@aws-sdk/util-middleware': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-endpoint@3.226.0: + resolution: {integrity: sha512-EvLFafjtUxTT0AC9p3aBQu1/fjhWdIeK58jIXaNFONfZ3F8QbEYUPuF/SqZvJM6cWfOO9qwYKkRDbCSTYhprIg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-serde': 3.226.0 + '@aws-sdk/protocol-http': 3.226.0 + '@aws-sdk/signature-v4': 3.226.0 + '@aws-sdk/types': 3.226.0 + '@aws-sdk/url-parser': 3.226.0 + '@aws-sdk/util-config-provider': 3.208.0 + '@aws-sdk/util-middleware': 3.226.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-endpoint@3.257.0: + resolution: {integrity: sha512-RQNQe/jeVuWZtXXfcOm+e3qMFICY6ERsXUrbt0rjHgvajZCklcrRJgxJSCwrcS7Le3nl9azFPMAMj9L7uSK28g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-serde': 3.257.0 + '@aws-sdk/protocol-http': 3.257.0 + '@aws-sdk/signature-v4': 3.257.0 + '@aws-sdk/types': 3.257.0 + '@aws-sdk/url-parser': 3.257.0 + '@aws-sdk/util-config-provider': 3.208.0 + '@aws-sdk/util-middleware': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-expect-continue@3.224.0: + resolution: {integrity: sha512-xgihNtu5dXzRqL0QrOuMLmSoji7BsKJ+rCXjW+X+Z1flYFV5UDY5PI0dgAlgWQDWZDyu17n4R5IIZUzb/aAI1g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-flexible-checksums@3.224.0: + resolution: {integrity: sha512-8umP3a1YNg5+sowQgzKNiq//vSVC53iTBzg8/oszstwIMYE9aNf4RKd/X/H9biBF/G05xdTjqNAQrAh54UbKrQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/crc32': 2.0.0 + '@aws-crypto/crc32c': 2.0.0 + '@aws-sdk/is-array-buffer': 3.201.0 + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-host-header@3.193.0: + resolution: {integrity: sha512-aegzj5oRWd//lmfmkzRmgG2b4l3140v8Ey4QkqCxcowvAEX5a7rh23yuKaGtmiePwv2RQalCKz+tN6JXCm8g6Q==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-host-header@3.224.0: + resolution: {integrity: sha512-4eL8EVhgxTjvdVs+P3SSEkoMXBte7hSQ/+kOZVNR5ze8QPnUiDpJMS2BQrMoA2INxX9tSqp6zTrDNMc3LNvKbQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-host-header@3.257.0: + resolution: {integrity: sha512-gEi9AJdJfRfU8Qr6HK1hfhxTzyV3Giq4B/h7um99hIFAT/GCg9xiPvAOKPo6UeuiKEv3b7RpSL4s6cBvnJMJBA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-location-constraint@3.224.0: + resolution: {integrity: sha512-FpgKNGzImgmHTbz4Hjc41GEH4/dASxz6sTtn5T+kFDsT1j7o21tpWlS6psoazTz9Yi3ichBo2yzYUaY3QxOFew==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-logger@3.193.0: + resolution: {integrity: sha512-D/h1pU5tAcyJpJ8ZeD1Sta0S9QZPcxERYRBiJdEl8VUrYwfy3Cl1WJedVOmd5nG73ZLRSyHeXHewb/ohge3yKQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-logger@3.224.0: + resolution: {integrity: sha512-AmvuezI1vGgKZDsA2slHZJ6nQMqogUyzK27wM03458a2JgFqZvWCUPSY/P+OZ0FpnFEC34/kvvF4bI54T0C5jA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-logger@3.257.0: + resolution: {integrity: sha512-8RDXW/VbMKBsXDfcCLmROZcWKyrekyiPa3J1aIaBy0tq9o4xpGoXw/lwwIrNVvISAFslb57rteup34bfn6ta6w==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-recursion-detection@3.193.0: + resolution: {integrity: sha512-fMWP76Q1GOb/9OzS1arizm6Dbfo02DPZ6xp7OoAN3PS6ybH3Eb47s/gP3jzgBPAITQacFj4St/4a06YWYrN3NA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-recursion-detection@3.224.0: + resolution: {integrity: sha512-ySTGlMvNaH5J77jYVVgwOF1ozz3Kp6f/wjTvivOcBR1zlRv0FXa1y033QMnrAAtKSNkzClXtNOycBM463QImJw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-recursion-detection@3.257.0: + resolution: {integrity: sha512-rUCih6zHh8k9Edf5N5Er4s508FYbwLM0MWTD2axzlj9TjLqEQ9OKED3wHaLffXSDzodd3oTAfJCLPbWQyoZ3ZQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-retry@3.193.0: + resolution: {integrity: sha512-zTQkHLBQBJi6ns655WYcYLyLPc1tgbEYU080Oc8zlveLUqoDn1ogkcmNhG7XMeQuBvWZBYN7J3/wFaXlDzeCKg==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.193.0 + '@aws-sdk/service-error-classification': 3.193.0 + '@aws-sdk/types': 3.193.0 + '@aws-sdk/util-middleware': 3.193.0 + tslib: 2.6.2 + uuid: 8.3.2 + dev: false + + /@aws-sdk/middleware-retry@3.224.0: + resolution: {integrity: sha512-zwl8rZZb5OWLzOnEW58RRklbehDfcdtD98qtgm0NLM9ErBALEEb2Y4MM5zhRiMtVjzrDw71+Mhk5+4TAlwJyXA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/service-error-classification': 3.224.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/util-middleware': 3.224.0 + tslib: 2.6.2 + uuid: 8.3.2 + dev: false + + /@aws-sdk/middleware-retry@3.259.0: + resolution: {integrity: sha512-pVh1g8e84MAi7eVtWLiiiCtn82LzxOP7+LxTRHatmgIeN22yGQBZILliPDJypUPvDYlwxI1ekiK+oPTcte0Uww==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.257.0 + '@aws-sdk/service-error-classification': 3.257.0 + '@aws-sdk/types': 3.257.0 + '@aws-sdk/util-middleware': 3.257.0 + '@aws-sdk/util-retry': 3.257.0 + tslib: 2.6.2 + uuid: 8.3.2 + dev: false + + /@aws-sdk/middleware-sdk-s3@3.224.0: + resolution: {integrity: sha512-SDyFandByU9UBQOxqFk8TCE0e9FPA/nr0FRjANxkIm24/zxk2yZbk3OUx/Zr7ibo28b5BqcQV69IClBOukPiEw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-bucket-endpoint': 3.224.0 + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/util-arn-parser': 3.208.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-sdk-sts@3.193.0: + resolution: {integrity: sha512-TafiDkeflUsnbNa89TLkDnAiRRp1gAaZLDAjt75AzriRKZnhtFfYUXWb+qAuN50T+CkJ/gZI9LHDZL5ogz/HxQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/middleware-signing': 3.193.0 + '@aws-sdk/property-provider': 3.193.0 + '@aws-sdk/protocol-http': 3.193.0 + '@aws-sdk/signature-v4': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-sdk-sts@3.224.0: + resolution: {integrity: sha512-rUoPPejj4N8S+P39ap9Iqbprl9L7LBlkuMHwMCqgeRJBhdI+1YeDfUekegJxceJv/BDXaoI2aSE0tCUS8rK0Ug==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-signing': 3.224.0 + '@aws-sdk/property-provider': 3.224.0 + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/signature-v4': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-sdk-sts@3.257.0: + resolution: {integrity: sha512-d6IJCLRi3O2tm4AFK60WNhIwmMmspj1WzKR1q1TaoPzoREPG2xg+Am18wZBRkCyYuRPPrbizmkvAmAJiUolMAw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-signing': 3.257.0 + '@aws-sdk/property-provider': 3.257.0 + '@aws-sdk/protocol-http': 3.257.0 + '@aws-sdk/signature-v4': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-serde@3.193.0: + resolution: {integrity: sha512-dH93EJYVztY+ZDPzSMRi9LfAZfKO+luH62raNy49hlNa4jiyE1Tc/+qwlmOEpfGsrtcZ9TgsON1uFF9sgBXXaA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-serde@3.224.0: + resolution: {integrity: sha512-4wHJ4DyhvyqQ853zfIw6sRw909VB+hFEqatmXYvO5OYap03Eed92wslsR2Gtfw1B2/zjDscPpwPyHoCIk30sHA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-serde@3.226.0: + resolution: {integrity: sha512-nPuOOAkSfx9TxzdKFx0X2bDlinOxGrqD7iof926K/AEflxGD1DBdcaDdjlYlPDW2CVE8LV/rAgbYuLxh/E/1VA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.226.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-serde@3.257.0: + resolution: {integrity: sha512-/JasfXPWFq24mnCrx9fxW/ISBSp07RJwhsF14qzm8Qy3Z0z470C+QRM6otTwAkYuuVt1wuLjja5agq3Jtzq7dQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-signing@3.193.0: + resolution: {integrity: sha512-obBoELGPf5ikvHYZwbzllLeuODiokdDfe92Ve2ufeOa/d8+xsmbqNzNdCTLNNTmr1tEIaEE7ngZVTOiHqAVhyw==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.193.0 + '@aws-sdk/protocol-http': 3.193.0 + '@aws-sdk/signature-v4': 3.193.0 + '@aws-sdk/types': 3.193.0 + '@aws-sdk/util-middleware': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-signing@3.224.0: + resolution: {integrity: sha512-6T+dybVn5EYsxkNc4eVKAeoj6x6FfRXkZWMRxkepDoOJufMUNTfpoDEl6PcgJU6Wq4odbqV737x/3j53VZc6dA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.224.0 + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/signature-v4': 3.224.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/util-middleware': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-signing@3.257.0: + resolution: {integrity: sha512-hCH3D83LHmm6nqmtNrGTWZCVjsQXrGHIXbd17/qrw7aPFvcAhsiiCncGFP+XsUXEKa2ZqcSNMUyPrx69ofNRZQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.257.0 + '@aws-sdk/protocol-http': 3.257.0 + '@aws-sdk/signature-v4': 3.257.0 + '@aws-sdk/types': 3.257.0 + '@aws-sdk/util-middleware': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-ssec@3.224.0: + resolution: {integrity: sha512-S9a3fvF0Lv/NnXKbh0cbqhzfVcCOU1pPeGKuDB/p7AWCoql/KSG52MGBU6jKcevCtWVUKpSkgJfs+xkKmSiXIA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-stack@3.193.0: + resolution: {integrity: sha512-Ix5d7gE6bZwFNIVf0dGnjYuymz1gjitNoAZDPpv1nEZlUMek/jcno5lmzWFzUZXY/azpbIyaPwq/wm/c69au5A==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-stack@3.224.0: + resolution: {integrity: sha512-8mBrc3nj4h6FnDWnxbjfFXUPr/7UIAaGAG15D27Z/KNFnMjOqNTtpkbcoh3QQHRLX3PjTuvzT5WCqXmgD2/oiw==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-stack@3.226.0: + resolution: {integrity: sha512-85wF29LvPvpoed60fZGDYLwv1Zpd/cM0C22WSSFPw1SSJeqO4gtFYyCg2squfT3KI6kF43IIkOCJ+L7GtryPug==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-stack@3.257.0: + resolution: {integrity: sha512-awg2F0SvwACBaw4HIObK8pQGfSqAc4Vy+YFzWSfZNVC35oRO6RsRdKHVU99lRC0LrT2Ptmfghl2DMPSrRDbvlQ==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-user-agent@3.193.0: + resolution: {integrity: sha512-0vT6F9NwYQK7ARUUJeHTUIUPnupsO3IbmjHSi1+clkssFlJm2UfmSGeafiWe4AYH3anATTvZEtcxX5DZT/ExbA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-user-agent@3.224.0: + resolution: {integrity: sha512-YXHC/n8k4qeIkqFVACPmF/QfJyKSOMD1HjM7iUZmJ9yGqDRFeGgn4o2Jktd0dor7sTv6pfUDkLqspxURAsokzA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-user-agent@3.257.0: + resolution: {integrity: sha512-37rt75LZyD0UWpbcFuxEGqwF3DZKSixQPl7AsDe6q3KtrO5gGQB+diH5vbY0txNNYyv5IK9WMwvY73mVmoWRmw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/node-config-provider@3.193.0: + resolution: {integrity: sha512-5RLdjQLH69ISRG8TX9klSLOpEySXxj+z9E9Em39HRvw0/rDcd8poCTADvjYIOqRVvMka0z/hm+elvUTIVn/DRw==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.193.0 + '@aws-sdk/shared-ini-file-loader': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/node-config-provider@3.224.0: + resolution: {integrity: sha512-ULv0Ao95vNEiwCreN9ZbZ5vntaGjdMLolCiyt3B2FDWbuOorZJR5QXFydPBpo4AQOh1y/S2MIUWLhz00DY364g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.224.0 + '@aws-sdk/shared-ini-file-loader': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/node-config-provider@3.259.0: + resolution: {integrity: sha512-DUOqr71oonBvM6yKPdhDBmraqgXHCFrVWFw7hc5ZNxL2wS/EsbKfGPJp+C+SUgpn1upIWPNnh/bNoLAbBkcLsA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.257.0 + '@aws-sdk/shared-ini-file-loader': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/node-http-handler@3.193.0: + resolution: {integrity: sha512-DP4BmFw64HOShgpAPEEMZedVnRmKKjHOwMEoXcnNlAkMXnYUFHiKvudYq87Q2AnSlT6OHkyMviB61gEvIk73dA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/abort-controller': 3.193.0 + '@aws-sdk/protocol-http': 3.193.0 + '@aws-sdk/querystring-builder': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/node-http-handler@3.224.0: + resolution: {integrity: sha512-8h4jWsfVRUcJKkqZ9msSN4LhldBpXdNlMcA8ku8IVEBHf5waxqpIhupwR0uCMmV3FDINLqkf/8EwEYAODeRjrw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/abort-controller': 3.224.0 + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/querystring-builder': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/node-http-handler@3.257.0: + resolution: {integrity: sha512-8KnWHVVwaGKyTlkTU9BSOAiSovNDoagxemU2l10QqBbzUCVpljCUMUkABEGRJ1yoQCl6DJ7RtNkAyZ8Ne/E15A==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/abort-controller': 3.257.0 + '@aws-sdk/protocol-http': 3.257.0 + '@aws-sdk/querystring-builder': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/property-provider@3.193.0: + resolution: {integrity: sha512-IaDR/PdZjKlAeSq2E/6u6nkPsZF9wvhHZckwH7uumq4ocWsWXFzaT+hKpV4YZPHx9n+K2YV4Gn/bDedpz99W1Q==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/property-provider@3.224.0: + resolution: {integrity: sha512-1F1Hepndlmj6wykNv0ynlS9YTaT3LRF/mqXhCRGLbCWSmCiaW9BUH/ddMdBZJiSw7kcPePKid5ueW84fAO/nKg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/property-provider@3.257.0: + resolution: {integrity: sha512-3rUbRAcF0GZ5PhDiXhS4yREfZ5hOEtvYEa9S/19OdM5eoypOaLU5XnFcCKfnccSP8SkdgpJujzxOMRWNWadlAQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/protocol-http@3.193.0: + resolution: {integrity: sha512-r0wbTwFJyXq0uiImI6giqG3g/RO1N/y4wwPA7qr7OC+KXJ0NkyVxIf6e7Vx8h06aM1ATtngbwJaMP59kVCp85A==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/protocol-http@3.224.0: + resolution: {integrity: sha512-myp31UkADbktZtIZLc4cNfr5zSNVJjPReoH37NPpvgREKOGg7ZB6Lb3UyKbjzrmIv985brMOunlMgIBIJhuPIg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/protocol-http@3.226.0: + resolution: {integrity: sha512-zWkVqiTA9RXL6y0hhfZc9bcU4DX2NI6Hw9IhQmSPeM59mdbPjJlY4bLlMr5YxywqO3yQ/ylNoAfrEzrDjlOSRg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.226.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/protocol-http@3.257.0: + resolution: {integrity: sha512-xt7LGOgZIvbLS3418AYQLacOqx+mo5j4mPiIMz7f6AaUg+/fBUgESVsncKDqxbEJVwwCXSka8Ca0cntJmoeMSw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/querystring-builder@3.193.0: + resolution: {integrity: sha512-PRaK6649iw0UO45UjUoiUzFcOKXZb8pMjjFJpqALpEvdZT3twxqhlPXujT7GWPKrSwO4uPLNnyYEtPY82wx2vw==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.193.0 + '@aws-sdk/util-uri-escape': 3.188.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/querystring-builder@3.224.0: + resolution: {integrity: sha512-Fwzt42wWRhf04TetQPqDL03jX5W2cAkRFQewOkIRYVFV17b72z4BFhKID6bpLEtNb4YagyllCWosNg1xooDURQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.224.0 + '@aws-sdk/util-uri-escape': 3.201.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/querystring-builder@3.257.0: + resolution: {integrity: sha512-mZHWLP7XIkzx1GIXO5WfX/iJ+aY9TWs02RE9FkdL2+by0HEMR65L3brQTbU1mIBJ7BjaPwYH24dljUOSABX7yg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.257.0 + '@aws-sdk/util-uri-escape': 3.201.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/querystring-parser@3.193.0: + resolution: {integrity: sha512-dGEPCe8SK4/td5dSpiaEI3SvT5eHXrbJWbLGyD4FL3n7WCGMy2xVWAB/yrgzD0GdLDjDa8L5vLVz6yT1P9i+hA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/querystring-parser@3.224.0: + resolution: {integrity: sha512-UIJZ76ClFtALXRIQS3Za4R76JTsjCYReSBEQ7ag7RF1jwVZLAggdfED9w3XDrN7jbaK6i+aI3Y+eFeq0sB2fcA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/querystring-parser@3.226.0: + resolution: {integrity: sha512-FzB+VrQ47KAFxiPt2YXrKZ8AOLZQqGTLCKHzx4bjxGmwgsjV8yIbtJiJhZLMcUQV4LtGeIY9ixIqQhGvnZHE4A==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.226.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/querystring-parser@3.257.0: + resolution: {integrity: sha512-UDrE1dEwWrWT8dG2VCrGYrPxCWOkZ1fPTPkjpkR4KZEdQDZBqU5gYZF2xPj8Nz7pjQVHFuW2wFm3XYEk56GEjg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/service-error-classification@3.193.0: + resolution: {integrity: sha512-bPnXVu8ErE1RfWVVQKc2TE7EuoImUi4dSPW9g80fGRzJdQNwXb636C+7OUuWvSDzmFwuBYqZza8GZjVd+rz2zQ==} + engines: {node: '>= 12.0.0'} + dev: false + + /@aws-sdk/service-error-classification@3.224.0: + resolution: {integrity: sha512-0bnbYtCe+vqtaGItL+1UzQPt+yZLbU8G/aIXPQUL7555jdnjnbAtczCbIcLAJUqlE/OLwRhQVGLKbau8QAdxgQ==} + engines: {node: '>=14.0.0'} + dev: false + + /@aws-sdk/service-error-classification@3.257.0: + resolution: {integrity: sha512-FAyR0XsueGkkqDtkP03cTJQk52NdQ9sZelLynmmlGPUP75LApRPvFe1riKrou6+LsDbwVNVffj6mbDfIcOhaOw==} + engines: {node: '>=14.0.0'} + dev: false + + /@aws-sdk/shared-ini-file-loader@3.193.0: + resolution: {integrity: sha512-hnvZup8RSpFXfah7Rrn6+lQJnAOCO+OiDJ2R/iMgZQh475GRQpLbu3cPhCOkjB14vVLygJtW8trK/0+zKq93bQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/shared-ini-file-loader@3.224.0: + resolution: {integrity: sha512-6a/XP3lRRcX5ic+bXzF2f644KERVqMx+s0JRrGsPAwTMaMiV0A7Ifl4HKggx6dnxh8j/MXUMsWMtuxt/kCu86A==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/shared-ini-file-loader@3.257.0: + resolution: {integrity: sha512-HNjC1+Wx3xHiJc+CP14GhIdVhfQGSjroAsWseRxAhONocA9Fl1ZX4hx7+sA5c9nOoMVOovi6ivJ/6lCRPTDRrQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/signature-v4-multi-region@3.224.0: + resolution: {integrity: sha512-xOW8rtEH2Rcadr+CFfiISZwcbf4jPdc4OvL6OiPsv+arndOhxk+4ZaRT2xic1FrVdYQypmSToRITYlZc9N7PjQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@aws-sdk/signature-v4-crt': ^3.118.0 + peerDependenciesMeta: + '@aws-sdk/signature-v4-crt': + optional: true + dependencies: + '@aws-sdk/protocol-http': 3.224.0 + '@aws-sdk/signature-v4': 3.224.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/util-arn-parser': 3.208.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/signature-v4@3.193.0: + resolution: {integrity: sha512-JEqqOB8wQZz6g1ERNUOIBFDFt8OJtz5G5Uh1CdkS5W66gyWnJEz/dE1hA2VTqqQwHGGEsIEV/hlzruU1lXsvFA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/is-array-buffer': 3.188.0 + '@aws-sdk/types': 3.193.0 + '@aws-sdk/util-hex-encoding': 3.188.0 + '@aws-sdk/util-middleware': 3.193.0 + '@aws-sdk/util-uri-escape': 3.188.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/signature-v4@3.224.0: + resolution: {integrity: sha512-+oq1iylYQOvdXXO7r18SEhXIZpLd3GvJhmoReX+yjvVq8mGevDAmQiw6lwFZ6748sOmH4CREWD5H9Snrj+zLMg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/is-array-buffer': 3.201.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/util-hex-encoding': 3.201.0 + '@aws-sdk/util-middleware': 3.224.0 + '@aws-sdk/util-uri-escape': 3.201.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/signature-v4@3.226.0: + resolution: {integrity: sha512-/R5q5agdPd7HJB68XMzpxrNPk158EHUvkFkuRu5Qf3kkkHebEzWEBlWoVpUe6ss4rP9Tqcue6xPuaftEmhjpYw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/is-array-buffer': 3.201.0 + '@aws-sdk/types': 3.226.0 + '@aws-sdk/util-hex-encoding': 3.201.0 + '@aws-sdk/util-middleware': 3.226.0 + '@aws-sdk/util-uri-escape': 3.201.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/signature-v4@3.257.0: + resolution: {integrity: sha512-aLQQN59X/D0+ShzPD3Anj5ntdMA/RFeNLOUCDyDvremViGi6yxUS98usQ/8bG5Rq0sW2GGMdbFUFmrDvqdiqEQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/is-array-buffer': 3.201.0 + '@aws-sdk/types': 3.257.0 + '@aws-sdk/util-hex-encoding': 3.201.0 + '@aws-sdk/util-middleware': 3.257.0 + '@aws-sdk/util-uri-escape': 3.201.0 + '@aws-sdk/util-utf8': 3.254.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/smithy-client@3.193.0: + resolution: {integrity: sha512-BY0jhfW76vyXr7ODMaKO3eyS98RSrZgOMl6DTQV9sk7eFP/MPVlG7p7nfX/CDIgPBIO1z0A0i2CVIzYur9uGgQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/middleware-stack': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/smithy-client@3.224.0: + resolution: {integrity: sha512-KXXzzrCBv8ewWdtm/aolZHr2f9NRZOcDutFaWXbfSptEsK50Zi9PNzB9ZVKUHyAXYjwJHb2Sl18WRrwIxH6H4g==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-stack': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/smithy-client@3.226.0: + resolution: {integrity: sha512-BWr1FhWSUhkSBp0TLzliD5AQBjA2Jmo9FlOOt+cBwd9BKkSGlGj+HgATYJ83Sjjg2+J6qvEZBxB78LKVHhorBw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-stack': 3.226.0 + '@aws-sdk/types': 3.226.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/smithy-client@3.261.0: + resolution: {integrity: sha512-j8XQEa3caZUVFVZfhJjaskw80O/tB+IXu84HMN44N7UkXaCFHirUsNjTDztJhnVXf/gKXzIqUqprfRnOvwLtIg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-stack': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/token-providers@3.224.0: + resolution: {integrity: sha512-cswWqA4n1v3JIALYRA8Tq/4uHcFpBg5cgi2khNHBCF/H09Hu3dynGup6Ji8cCzf3fTak4eBQipcWaWUGE0hTGw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/client-sso-oidc': 3.224.0 + '@aws-sdk/property-provider': 3.224.0 + '@aws-sdk/shared-ini-file-loader': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/token-providers@3.261.0: + resolution: {integrity: sha512-Vi/GOnx8rPvQz5TdJJl5CwpTX6uRsSE3fzh94O4FEAIxIFtb4P5juqg92+2CJ81C7iNduB6eEeSHtwWUylypXQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/client-sso-oidc': 3.261.0 + '@aws-sdk/property-provider': 3.257.0 + '@aws-sdk/shared-ini-file-loader': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/types@3.193.0: + resolution: {integrity: sha512-LV/wcPolRZKORrcHwkH59QMCkiDR5sM+9ZtuTxvyUGG2QFW/kjoxs08fUF10OWNJMrotBI+czDc5QJRgN8BlAw==} + engines: {node: '>= 12.0.0'} + dev: false + + /@aws-sdk/types@3.224.0: + resolution: {integrity: sha512-7te9gRondKPjEebyiPYn59Kr5LZOL48HXC05TzFIN/JXwWPJbQpROBPeKd53V1aRdr3vSQhDY01a+vDOBBrEUQ==} + engines: {node: '>=14.0.0'} + dev: false + + /@aws-sdk/types@3.226.0: + resolution: {integrity: sha512-MmmNHrWeO4man7wpOwrAhXlevqtOV9ZLcH4RhnG5LmRce0RFOApx24HoKENfFCcOyCm5LQBlsXCqi0dZWDWU0A==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/types@3.257.0: + resolution: {integrity: sha512-LmqXuBQBGeaGi/3Rp7XiEX1B5IPO2UUfBVvu0wwGqVsmstT0SbOVDZGPmxygACbm64n+PRx3uTSDefRfoiWYZg==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/url-parser@3.193.0: + resolution: {integrity: sha512-hwD1koJlOu2a6GvaSbNbdo7I6a3tmrsNTZr8bCjAcbqpc5pDThcpnl/Uaz3zHmMPs92U8I6BvWoK6pH8By06qw==} + dependencies: + '@aws-sdk/querystring-parser': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/url-parser@3.224.0: + resolution: {integrity: sha512-DGQoiOxRVq9eEbmcGF7oz/htcHxFtLlUTzKbaX1gFuh1kmhRQwJIzz6vkrMdxOgPjvUYMJuMEcYnsHolDNWbMg==} + dependencies: + '@aws-sdk/querystring-parser': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/url-parser@3.226.0: + resolution: {integrity: sha512-p5RLE0QWyP0OcTOLmFcLdVgUcUEzmEfmdrnOxyNzomcYb0p3vUagA5zfa1HVK2azsQJFBv28GfvMnba9bGhObg==} + dependencies: + '@aws-sdk/querystring-parser': 3.226.0 + '@aws-sdk/types': 3.226.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/url-parser@3.257.0: + resolution: {integrity: sha512-Qe/AcFe/NFZHa6cN2afXEQn9ehXxh57dWGdRjfjd2lQqNV4WW1R2pl2Tm1ZJ1dwuCNLJi4NHLMk8lrD3QQ8rdg==} + dependencies: + '@aws-sdk/querystring-parser': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-arn-parser@3.208.0: + resolution: {integrity: sha512-QV4af+kscova9dv4VuHOgH8wEr/IIYHDGcnyVtkUEqahCejWr1Kuk+SBK0xMwnZY5LSycOtQ8aeqHOn9qOjZtA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-base64-browser@3.188.0: + resolution: {integrity: sha512-qlH+5NZBLiyKziL335BEPedYxX6j+p7KFRWXvDQox9S+s+gLCayednpK+fteOhBenCcR9fUZOVuAPScy1I8qCg==} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-base64-node@3.188.0: + resolution: {integrity: sha512-r1dccRsRjKq+OhVRUfqFiW3sGgZBjHbMeHLbrAs9jrOjU2PTQ8PSzAXLvX/9lmp7YjmX17Qvlsg0NCr1tbB9OA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/util-buffer-from': 3.188.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-base64@3.208.0: + resolution: {integrity: sha512-PQniZph5A6N7uuEOQi+1hnMz/FSOK/8kMFyFO+4DgA1dZ5pcKcn5wiFwHkcTb/BsgVqQa3Jx0VHNnvhlS8JyTg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/util-buffer-from': 3.208.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-body-length-browser@3.188.0: + resolution: {integrity: sha512-8VpnwFWXhnZ/iRSl9mTf+VKOX9wDE8QtN4bj9pBfxwf90H1X7E8T6NkiZD3k+HubYf2J94e7DbeHs7fuCPW5Qg==} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-body-length-node@3.188.0: + resolution: {integrity: sha512-XwqP3vxk60MKp4YDdvDeCD6BPOiG2e+/Ou4AofZOy5/toB6NKz2pFNibQIUg2+jc7mPMnGnvOW3MQEgSJ+gu/Q==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-body-length-node@3.208.0: + resolution: {integrity: sha512-3zj50e5g7t/MQf53SsuuSf0hEELzMtD8RX8C76f12OSRo2Bca4FLLYHe0TZbxcfQHom8/hOaeZEyTyMogMglqg==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-buffer-from@3.188.0: + resolution: {integrity: sha512-NX1WXZ8TH20IZb4jPFT2CnLKSqZWddGxtfiWxD9M47YOtq/SSQeR82fhqqVjJn4P8w2F5E28f+Du4ntg/sGcxA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/is-array-buffer': 3.188.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-buffer-from@3.208.0: + resolution: {integrity: sha512-7L0XUixNEFcLUGPeBF35enCvB9Xl+K6SQsmbrPk1P3mlV9mguWSDQqbOBwY1Ir0OVbD6H/ZOQU7hI/9RtRI0Zw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/is-array-buffer': 3.201.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-config-provider@3.188.0: + resolution: {integrity: sha512-LBA7tLbi7v4uvbOJhSnjJrxbcRifKK/1ZVK94JTV2MNSCCyNkFotyEI5UWDl10YKriTIUyf7o5cakpiDZ3O4xg==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-config-provider@3.208.0: + resolution: {integrity: sha512-DSRqwrERUsT34ug+anlMBIFooBEGwM8GejC7q00Y/9IPrQy50KnG5PW2NiTjuLKNi7pdEOlwTSEocJE15eDZIg==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-defaults-mode-browser@3.193.0: + resolution: {integrity: sha512-9riQKFrSJcsNAMnPA/3ltpSxNykeO20klE/UKjxEoD7UWjxLwsPK22UJjFwMRaHoAFcZD0LU/SgPxbC0ktCYCg==} + engines: {node: '>= 10.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.193.0 + '@aws-sdk/types': 3.193.0 + bowser: 2.11.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-defaults-mode-browser@3.224.0: + resolution: {integrity: sha512-umk+A/pmlbuyvDCgdndgJUa0xitcTUF7XoUt/3qDTpNbzR5Dzgdbz74BgXUAEBJ8kPP5pCo2VE1ZD7fxqYU/dQ==} + engines: {node: '>= 10.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.224.0 + '@aws-sdk/types': 3.224.0 + bowser: 2.11.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-defaults-mode-browser@3.261.0: + resolution: {integrity: sha512-lX3X1NfzQVV6cakepGV24uRcqevlDnQ8VgaCV8dhnw1FVThueFigyoFaUA02+uRXbV9KIbNWkEvweNtm2wvyDw==} + engines: {node: '>= 10.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.257.0 + '@aws-sdk/types': 3.257.0 + bowser: 2.11.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-defaults-mode-node@3.193.0: + resolution: {integrity: sha512-occQmckvPRiM4YQIZnulfKKKjykGKWloa5ByGC5gOEGlyeP9zJpfs4zc/M2kArTAt+d2r3wkBtsKe5yKSlVEhA==} + engines: {node: '>= 10.0.0'} + dependencies: + '@aws-sdk/config-resolver': 3.193.0 + '@aws-sdk/credential-provider-imds': 3.193.0 + '@aws-sdk/node-config-provider': 3.193.0 + '@aws-sdk/property-provider': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-defaults-mode-node@3.224.0: + resolution: {integrity: sha512-ZJQJ1McbQ5Rnf5foCFAKHT8Cbwg4IbM+bb6fCkHRJFH9AXEvwc+hPtSYf0KuI7TmoZFj9WG5JOE9Ns6g7lRHSA==} + engines: {node: '>= 10.0.0'} + dependencies: + '@aws-sdk/config-resolver': 3.224.0 + '@aws-sdk/credential-provider-imds': 3.224.0 + '@aws-sdk/node-config-provider': 3.224.0 + '@aws-sdk/property-provider': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-defaults-mode-node@3.261.0: + resolution: {integrity: sha512-4AK6yu4bOmHSocUdbGoEHbNXB09UA58ON2HBHY4NxMBuFBAd9XB2tYiyhce+Cm+o+lHbS8oQnw0VZw16WMzzew==} + engines: {node: '>= 10.0.0'} + dependencies: + '@aws-sdk/config-resolver': 3.259.0 + '@aws-sdk/credential-provider-imds': 3.259.0 + '@aws-sdk/node-config-provider': 3.259.0 + '@aws-sdk/property-provider': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-endpoints@3.196.0: + resolution: {integrity: sha512-X+DOpRUy/ij49a0GQtggk09oyIQGn0mhER6PbMT69IufZPIg3D5fC5FPEp8bfsPkb70fTEYQEsj/X/rgMQJKsA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-endpoints@3.224.0: + resolution: {integrity: sha512-k5hHbk7AP/cajw5rF7wmKP39B0WQMFdxrn8dcVOHVK0FZeKbaGCEmOf3AYXrQhswR9Xo815Rqffoml9B1z3bCA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-endpoints@3.257.0: + resolution: {integrity: sha512-3bvmRn5XGYzPPWjLuvHBKdJOb+fijnb8Ungu9bfXnTYFsng/ndHUWeHC22O/p8w3OWoRYUIMaZHxdxe27BFozg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-hex-encoding@3.188.0: + resolution: {integrity: sha512-QyWovTtjQ2RYxqVM+STPh65owSqzuXURnfoof778spyX4iQ4z46wOge1YV2ZtwS8w5LWd9eeVvDrLu5POPYOnA==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-hex-encoding@3.201.0: + resolution: {integrity: sha512-7t1vR1pVxKx0motd3X9rI3m/xNp78p3sHtP5yo4NP4ARpxyJ0fokBomY8ScaH2D/B+U5o9ARxldJUdMqyBlJcA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-locate-window@3.310.0: + resolution: {integrity: sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-middleware@3.193.0: + resolution: {integrity: sha512-+aC6pmkcGgpxaMWCH/FXTsGWl2W342oQGs1OYKGi+W8z9UguXrqamWjdkdMqgunvj9qOEG2KBMKz1FWFFZlUyA==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-middleware@3.224.0: + resolution: {integrity: sha512-yA20k9sJdFgs7buVilWExUSJ/Ecr5UJRNQlmgzIpBo9kh5x/N8WyB4kN5MQw5UAA1UZ+j3jmA9+YLFT/mbX3IQ==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-middleware@3.226.0: + resolution: {integrity: sha512-B96CQnwX4gRvQdaQkdUtqvDPkrptV5+va6FVeJOocU/DbSYMAScLxtR3peMS8cnlOT6nL1Eoa42OI9AfZz1VwQ==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-middleware@3.257.0: + resolution: {integrity: sha512-F9ieon8B8eGVs5tyZtAIG3DZEObDvujkspho0qRbUTHUosM0ylJLsMU800fmC/uRHLRrZvb/RSp59+kNDwSAMw==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-retry@3.257.0: + resolution: {integrity: sha512-l9TOsOAYtZxwW3q5fQKW4rsD9t2HVaBfQ4zBamHkNTfB4vBVvCnz4oxkvSvA2MlxCA6am+K1K/oj917Tpqk53g==} + engines: {node: '>= 14.0.0'} + dependencies: + '@aws-sdk/service-error-classification': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-stream-browser@3.224.0: + resolution: {integrity: sha512-JS+C8CyxVFMQ69P4QIDTrzkhseEFCVFy2YHZYlCx3M5P+L1/PQHebTETYFMmO9ThY8TRXmYZDJHv79guvV+saQ==} + dependencies: + '@aws-sdk/fetch-http-handler': 3.224.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/util-base64': 3.208.0 + '@aws-sdk/util-hex-encoding': 3.201.0 + '@aws-sdk/util-utf8-browser': 3.188.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-stream-node@3.224.0: + resolution: {integrity: sha512-ztvZHJJg9/BwUrKnSz3jV6T8oOdxA1MRypK2zqTdjoPU9u/8CFQ2p0gszBApMjyxCnLWo1oM5oiMwzz1ufDrlA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/node-http-handler': 3.224.0 + '@aws-sdk/types': 3.224.0 + '@aws-sdk/util-buffer-from': 3.208.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-uri-escape@3.188.0: + resolution: {integrity: sha512-4Y6AYZMT483Tiuq8dxz5WHIiPNdSFPGrl6tRTo2Oi2FcwypwmFhqgEGcqxeXDUJktvaCBxeA08DLr/AemVhPCg==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-uri-escape@3.201.0: + resolution: {integrity: sha512-TeTWbGx4LU2c5rx0obHeDFeO9HvwYwQtMh1yniBz00pQb6Qt6YVOETVQikRZ+XRQwEyCg/dA375UplIpiy54mA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-user-agent-browser@3.193.0: + resolution: {integrity: sha512-1EkGYsUtOMEyJG/UBIR4PtmO3lVjKNoUImoMpLtEucoGbWz5RG9zFSwLevjFyFs5roUBFlxkSpTMo8xQ3aRzQg==} + dependencies: + '@aws-sdk/types': 3.193.0 + bowser: 2.11.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-user-agent-browser@3.224.0: + resolution: {integrity: sha512-Dm/30cLUIM1Oam4V//m9sPrXyGOKFslUXP7Mz2AlR1HelUYoreWAIe7Rx44HR6PaXyZmjW5K0ItmcJ7tCgyMpw==} + dependencies: + '@aws-sdk/types': 3.224.0 + bowser: 2.11.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-user-agent-browser@3.257.0: + resolution: {integrity: sha512-YdavWK6/8Cw6mypEgysGGX/dT9p9qnzFbnN5PQsUY+JJk2Nx8fKFydjGiQ+6rWPeW17RAv9mmbboh9uPVWxVlw==} + dependencies: + '@aws-sdk/types': 3.257.0 + bowser: 2.11.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-user-agent-node@3.193.0: + resolution: {integrity: sha512-G/2/1cSgsxVtREAm8Eq8Duib5PXzXknFRHuDpAxJ5++lsJMXoYMReS278KgV54cojOkAVfcODDTqmY3Av0WHhQ==} + engines: {node: '>= 12.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + dependencies: + '@aws-sdk/node-config-provider': 3.193.0 + '@aws-sdk/types': 3.193.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-user-agent-node@3.224.0: + resolution: {integrity: sha512-BTj0vPorfT7AJzv6RxJHrnAKdIHwZmGjp5TFFaCYgFkHAPsyCPceSdZUjBRW+HbiwEwKfoHOXLGjnOBSqddZKg==} + engines: {node: '>=14.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + dependencies: + '@aws-sdk/node-config-provider': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-user-agent-node@3.259.0: + resolution: {integrity: sha512-R0VTmNs+ySDDebU98BUbsLyeIM5YmAEr9esPpy15XfSy3AWmAeru8nLlztdaLilHZzLIDzvM2t7NGk/FzZFCvA==} + engines: {node: '>=14.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + dependencies: + '@aws-sdk/node-config-provider': 3.259.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-utf8-browser@3.188.0: + resolution: {integrity: sha512-jt627x0+jE+Ydr9NwkFstg3cUvgWh56qdaqAMDsqgRlKD21md/6G226z/Qxl7lb1VEW2LlmCx43ai/37Qwcj2Q==} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-utf8-browser@3.259.0: + resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-utf8-node@3.188.0: + resolution: {integrity: sha512-hCgP4+C0Lekjpjt2zFJ2R/iHes5sBGljXa5bScOFAEkRUc0Qw0VNgTv7LpEbIOAwGmqyxBoCwBW0YHPW1DfmYQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/util-buffer-from': 3.188.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-utf8-node@3.208.0: + resolution: {integrity: sha512-jKY87Acv0yWBdFxx6bveagy5FYjz+dtV8IPT7ay1E2WPWH1czoIdMAkc8tSInK31T6CRnHWkLZ1qYwCbgRfERQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/util-buffer-from': 3.208.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-utf8@3.254.0: + resolution: {integrity: sha512-14Kso/eIt5/qfIBmhEL9L1IfyUqswjSTqO2mY7KOzUZ9SZbwn3rpxmtkhmATkRjD7XIlLKaxBkI7tU9Zjzj8Kw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/util-buffer-from': 3.208.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-waiter@3.224.0: + resolution: {integrity: sha512-+SNItYzUSPa8PV1iWwOi3637ztlczJNa2pZ/R1nWf2N8sAmk0BXzGJISv/GKvzPDyAz+uOpT549e8P6rRLZedA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/abort-controller': 3.224.0 + '@aws-sdk/types': 3.224.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-waiter@3.257.0: + resolution: {integrity: sha512-Fr6of3EDOcXVDs5534o7VsJMXdybB0uLy2LzeFAVSwGOY3geKhIquBAiUDqCVu9B+iTldrC0rQ9NIM7ZSpPG8w==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/abort-controller': 3.257.0 + '@aws-sdk/types': 3.257.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/xml-builder@3.201.0: + resolution: {integrity: sha512-brRdB1wwMgjWEnOQsv7zSUhIQuh7DEicrfslAqHop4S4FtSI3GQAShpQqgOpMTNFYcpaWKmE/Y1MJmNY7xLCnw==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@babel/code-frame@7.22.10: + resolution: {integrity: sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.22.10 + chalk: 2.4.2 + dev: true + + /@babel/compat-data@7.22.9: + resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.18.6: + resolution: {integrity: sha512-cQbWBpxcbbs/IUredIPkHiAGULLV8iwgNRMFzvbhEXISp4f3rUUXE5+TIw6KwUWUR3DwyI6gmBRnmAtYaWehwQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.22.10 + '@babel/generator': 7.22.10 + '@babel/helper-compilation-targets': 7.22.10 + '@babel/helper-module-transforms': 7.22.9(@babel/core@7.18.6) + '@babel/helpers': 7.22.10 + '@babel/parser': 7.22.10 + '@babel/template': 7.22.5 + '@babel/traverse': 7.22.10 + '@babel/types': 7.22.10 + convert-source-map: 1.9.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator@7.22.10: + resolution: {integrity: sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.10 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.19 + jsesc: 2.5.2 + dev: true + + /@babel/helper-annotate-as-pure@7.22.5: + resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.10 + dev: true + + /@babel/helper-builder-binary-assignment-operator-visitor@7.22.10: + resolution: {integrity: sha512-Av0qubwDQxC56DoUReVDeLfMEjYYSN1nZrTUrWkXd7hpU73ymRANkbuDm3yni9npkn+RXy9nNbEJZEzXr7xrfQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.10 + dev: true + + /@babel/helper-compilation-targets@7.22.10: + resolution: {integrity: sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.22.9 + '@babel/helper-validator-option': 7.22.5 + browserslist: 4.21.10 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-create-class-features-plugin@7.22.10(@babel/core@7.18.6): + resolution: {integrity: sha512-5IBb77txKYQPpOEdUdIhBx8VrZyDCQ+H82H0+5dX1TmuscP5vJKEE3cKurjtIw/vFwzbVH48VweE78kVDBrqjA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-function-name': 7.22.5 + '@babel/helper-member-expression-to-functions': 7.22.5 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-replace-supers': 7.22.9(@babel/core@7.18.6) + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + semver: 6.3.1 + dev: true + + /@babel/helper-create-regexp-features-plugin@7.22.9(@babel/core@7.18.6): + resolution: {integrity: sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-annotate-as-pure': 7.22.5 + regexpu-core: 5.3.2 + semver: 6.3.1 + dev: true + + /@babel/helper-define-polyfill-provider@0.3.3(@babel/core@7.18.6): + resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==} + peerDependencies: + '@babel/core': ^7.4.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-compilation-targets': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 + debug: 4.3.4 + lodash.debounce: 4.0.8 + resolve: 1.22.4 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-environment-visitor@7.22.5: + resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-function-name@7.22.5: + resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.5 + '@babel/types': 7.22.10 + dev: true + + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.10 + dev: true + + /@babel/helper-member-expression-to-functions@7.22.5: + resolution: {integrity: sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.10 + dev: true + + /@babel/helper-module-imports@7.22.5: + resolution: {integrity: sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.10 + dev: true + + /@babel/helper-module-transforms@7.22.9(@babel/core@7.18.6): + resolution: {integrity: sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-module-imports': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.5 + dev: true + + /@babel/helper-optimise-call-expression@7.22.5: + resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.10 + dev: true + + /@babel/helper-plugin-utils@7.22.5: + resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-remap-async-to-generator@7.22.9(@babel/core@7.18.6): + resolution: {integrity: sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-wrap-function': 7.22.10 + dev: true + + /@babel/helper-replace-supers@7.22.9(@babel/core@7.18.6): + resolution: {integrity: sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-member-expression-to-functions': 7.22.5 + '@babel/helper-optimise-call-expression': 7.22.5 + dev: true + + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.10 + dev: true + + /@babel/helper-skip-transparent-expression-wrappers@7.22.5: + resolution: {integrity: sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.10 + dev: true + + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.22.10 + dev: true + + /@babel/helper-string-parser@7.22.5: + resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.22.5: + resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.22.5: + resolution: {integrity: sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-wrap-function@7.22.10: + resolution: {integrity: sha512-OnMhjWjuGYtdoO3FmsEFWvBStBAe2QOgwOLsLNDjN+aaiMD8InJk1/O3HSD8lkqTjCgg5YI34Tz15KNNA3p+nQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-function-name': 7.22.5 + '@babel/template': 7.22.5 + '@babel/types': 7.22.10 + dev: true + + /@babel/helpers@7.22.10: + resolution: {integrity: sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.5 + '@babel/traverse': 7.22.10 + '@babel/types': 7.22.10 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/highlight@7.22.10: + resolution: {integrity: sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.5 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@babel/parser@7.22.10: + resolution: {integrity: sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.22.10 + dev: true + + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-transform-optional-chaining': 7.22.10(@babel/core@7.18.6) + dev: true + + /@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.18.6): + resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-remap-async-to-generator': 7.22.9(@babel/core@7.18.6) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.18.6) + dev: true + + /@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.18.6): + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-create-class-features-plugin': 7.22.10(@babel/core@7.18.6) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-proposal-class-static-block@7.21.0(@babel/core@7.18.6): + resolution: {integrity: sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-create-class-features-plugin': 7.22.10(@babel/core@7.18.6) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.18.6) + dev: true + + /@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.18.6): + resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.18.6) + dev: true + + /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.18.6): + resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.18.6) + dev: true + + /@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.18.6): + resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.18.6) + dev: true + + /@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.18.6): + resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.18.6) + dev: true + + /@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.18.6): + resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.18.6) + dev: true + + /@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.18.6): + resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.18.6) + dev: true + + /@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.18.6): + resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.22.9 + '@babel/core': 7.18.6 + '@babel/helper-compilation-targets': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.18.6) + '@babel/plugin-transform-parameters': 7.22.5(@babel/core@7.18.6) + dev: true + + /@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.18.6): + resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.18.6) + dev: true + + /@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.18.6): + resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.18.6) + dev: true + + /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.18.6): + resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-create-class-features-plugin': 7.22.10(@babel/core@7.18.6) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-proposal-private-property-in-object@7.21.11(@babel/core@7.18.6): + resolution: {integrity: sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-create-class-features-plugin': 7.22.10(@babel/core@7.18.6) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.18.6) + dev: true + + /@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.18.6): + resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} + engines: {node: '>=4'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-create-regexp-features-plugin': 7.22.9(@babel/core@7.18.6) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.18.6): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.18.6): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.18.6): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.18.6): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.18.6): + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.18.6): + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-import-assertions@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.18.6): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.18.6): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.18.6): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.18.6): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.18.6): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.18.6): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.18.6): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.18.6): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.18.6): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.18.6): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-typescript@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-arrow-functions@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-async-to-generator@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-module-imports': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-remap-async-to-generator': 7.22.9(@babel/core@7.18.6) + dev: true + + /@babel/plugin-transform-block-scoped-functions@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-block-scoping@7.22.10(@babel/core@7.18.6): + resolution: {integrity: sha512-1+kVpGAOOI1Albt6Vse7c8pHzcZQdQKW+wJH+g8mCaszOdDVwRXa/slHPqIw+oJAJANTKDMuM2cBdV0Dg618Vg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-classes@7.22.6(@babel/core@7.18.6): + resolution: {integrity: sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-compilation-targets': 7.22.10 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-function-name': 7.22.5 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.9(@babel/core@7.18.6) + '@babel/helper-split-export-declaration': 7.22.6 + globals: 11.12.0 + dev: true + + /@babel/plugin-transform-computed-properties@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/template': 7.22.5 + dev: true + + /@babel/plugin-transform-destructuring@7.22.10(@babel/core@7.18.6): + resolution: {integrity: sha512-dPJrL0VOyxqLM9sritNbMSGx/teueHF/htMKrPT7DNxccXxRDPYqlgPFFdr8u+F+qUZOkZoXue/6rL5O5GduEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-dotall-regex@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-create-regexp-features-plugin': 7.22.9(@babel/core@7.18.6) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-duplicate-keys@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-exponentiation-operator@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-for-of@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-function-name@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-compilation-targets': 7.22.10 + '@babel/helper-function-name': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-literals@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-member-expression-literals@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-modules-amd@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-module-transforms': 7.22.9(@babel/core@7.18.6) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-modules-commonjs@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-module-transforms': 7.22.9(@babel/core@7.18.6) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + dev: true + + /@babel/plugin-transform-modules-systemjs@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-module-transforms': 7.22.9(@babel/core@7.18.6) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-identifier': 7.22.5 + dev: true + + /@babel/plugin-transform-modules-umd@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-module-transforms': 7.22.9(@babel/core@7.18.6) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-create-regexp-features-plugin': 7.22.9(@babel/core@7.18.6) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-new-target@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-object-super@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.9(@babel/core@7.18.6) + dev: true + + /@babel/plugin-transform-optional-chaining@7.22.10(@babel/core@7.18.6): + resolution: {integrity: sha512-MMkQqZAZ+MGj+jGTG3OTuhKeBpNcO+0oCEbrGNEaOmiEn+1MzRyQlYsruGiU8RTK3zV6XwrVJTmwiDOyYK6J9g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.18.6) + dev: true + + /@babel/plugin-transform-parameters@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-property-literals@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-regenerator@7.22.10(@babel/core@7.18.6): + resolution: {integrity: sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + regenerator-transform: 0.15.2 + dev: true + + /@babel/plugin-transform-reserved-words@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-shorthand-properties@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-spread@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + dev: true + + /@babel/plugin-transform-sticky-regex@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-template-literals@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-typeof-symbol@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-unicode-escapes@7.22.10(@babel/core@7.18.6): + resolution: {integrity: sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-unicode-regex@7.22.5(@babel/core@7.18.6): + resolution: {integrity: sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-create-regexp-features-plugin': 7.22.9(@babel/core@7.18.6) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/preset-env@7.18.6(@babel/core@7.18.6): + resolution: {integrity: sha512-WrthhuIIYKrEFAwttYzgRNQ5hULGmwTj+D6l7Zdfsv5M7IWV/OZbUfbeL++Qrzx1nVJwWROIFhCHRYQV4xbPNw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.22.9 + '@babel/core': 7.18.6 + '@babel/helper-compilation-targets': 7.22.10 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.5 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-proposal-async-generator-functions': 7.20.7(@babel/core@7.18.6) + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.18.6) + '@babel/plugin-proposal-class-static-block': 7.21.0(@babel/core@7.18.6) + '@babel/plugin-proposal-dynamic-import': 7.18.6(@babel/core@7.18.6) + '@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.18.6) + '@babel/plugin-proposal-json-strings': 7.18.6(@babel/core@7.18.6) + '@babel/plugin-proposal-logical-assignment-operators': 7.20.7(@babel/core@7.18.6) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.18.6) + '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.18.6) + '@babel/plugin-proposal-object-rest-spread': 7.20.7(@babel/core@7.18.6) + '@babel/plugin-proposal-optional-catch-binding': 7.18.6(@babel/core@7.18.6) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.18.6) + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.18.6) + '@babel/plugin-proposal-private-property-in-object': 7.21.11(@babel/core@7.18.6) + '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.18.6) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.18.6) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.18.6) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.18.6) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.18.6) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.18.6) + '@babel/plugin-syntax-import-assertions': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.18.6) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.18.6) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.18.6) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.18.6) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.18.6) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.18.6) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.18.6) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.18.6) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.18.6) + '@babel/plugin-transform-arrow-functions': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-block-scoped-functions': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-block-scoping': 7.22.10(@babel/core@7.18.6) + '@babel/plugin-transform-classes': 7.22.6(@babel/core@7.18.6) + '@babel/plugin-transform-computed-properties': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-destructuring': 7.22.10(@babel/core@7.18.6) + '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-duplicate-keys': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-exponentiation-operator': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-for-of': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-function-name': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-literals': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-member-expression-literals': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-modules-amd': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-modules-commonjs': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-modules-systemjs': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-modules-umd': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-new-target': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-object-super': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-parameters': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-property-literals': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-regenerator': 7.22.10(@babel/core@7.18.6) + '@babel/plugin-transform-reserved-words': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-shorthand-properties': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-spread': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-sticky-regex': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-template-literals': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-typeof-symbol': 7.22.5(@babel/core@7.18.6) + '@babel/plugin-transform-unicode-escapes': 7.22.10(@babel/core@7.18.6) + '@babel/plugin-transform-unicode-regex': 7.22.5(@babel/core@7.18.6) + '@babel/preset-modules': 0.1.6(@babel/core@7.18.6) + '@babel/types': 7.22.10 + babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.18.6) + babel-plugin-polyfill-corejs3: 0.5.3(@babel/core@7.18.6) + babel-plugin-polyfill-regenerator: 0.3.1(@babel/core@7.18.6) + core-js-compat: 3.32.1 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/preset-modules@0.1.6(@babel/core@7.18.6): + resolution: {integrity: sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.18.6) + '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.18.6) + '@babel/types': 7.22.10 + esutils: 2.0.3 + dev: true + + /@babel/regjsgen@0.8.0: + resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} + dev: true + + /@babel/runtime@7.22.10: + resolution: {integrity: sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + dev: true + + /@babel/template@7.22.5: + resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.22.10 + '@babel/parser': 7.22.10 + '@babel/types': 7.22.10 + dev: true + + /@babel/traverse@7.22.10: + resolution: {integrity: sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.22.10 + '@babel/generator': 7.22.10 + '@babel/helper-environment-visitor': 7.22.5 + '@babel/helper-function-name': 7.22.5 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.22.10 + '@babel/types': 7.22.10 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.22.10: + resolution: {integrity: sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.22.5 + '@babel/helper-validator-identifier': 7.22.5 + to-fast-properties: 2.0.0 + dev: true + + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true + + /@cbor-extract/cbor-extract-darwin-arm64@2.1.1: + resolution: {integrity: sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@cbor-extract/cbor-extract-darwin-x64@2.1.1: + resolution: {integrity: sha512-h6KFOzqk8jXTvkOftyRIWGrd7sKQzQv2jVdTL9nKSf3D2drCvQB/LHUxAOpPXo3pv2clDtKs3xnHalpEh3rDsw==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@cbor-extract/cbor-extract-linux-arm64@2.1.1: + resolution: {integrity: sha512-SxAaRcYf8S0QHaMc7gvRSiTSr7nUYMqbUdErBEu+HYA4Q6UNydx1VwFE68hGcp1qvxcy9yT5U7gA+a5XikfwSQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@cbor-extract/cbor-extract-linux-arm@2.1.1: + resolution: {integrity: sha512-ds0uikdcIGUjPyraV4oJqyVE5gl/qYBpa/Wnh6l6xLE2lj/hwnjT2XcZCChdXwW/YFZ1LUHs6waoYN8PmK0nKQ==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@cbor-extract/cbor-extract-linux-x64@2.1.1: + resolution: {integrity: sha512-GVK+8fNIE9lJQHAlhOROYiI0Yd4bAZ4u++C2ZjlkS3YmO6hi+FUxe6Dqm+OKWTcMpL/l71N6CQAmaRcb4zyJuA==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@cbor-extract/cbor-extract-win32-x64@2.1.1: + resolution: {integrity: sha512-2Niq1C41dCRIDeD8LddiH+mxGlO7HJ612Ll3D/E73ZWBmycued+8ghTr/Ho3CMOWPUEr08XtyBMVXAjqF+TcKw==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + + /@datadog/native-appsec@2.0.0: + resolution: {integrity: sha512-XHARZ6MVgbnfOUO6/F3ZoZ7poXHJCNYFlgcyS2Xetuk9ITA5bfcooX2B2F7tReVB+RLJ+j8bsm0t55SyF04KDw==} + engines: {node: '>=12'} + requiresBuild: true + dependencies: + node-gyp-build: 3.9.0 + dev: false + + /@datadog/native-iast-rewriter@1.1.2: + resolution: {integrity: sha512-pigRfRtAjZjMjqIXyXb98S4aDnuHz/EmqpoxAajFZsNjBLM87YonwSY5zoBdCsOyA46ddKOJRoCQd5ZalpOFMQ==} + engines: {node: '>= 10'} + dependencies: + node-gyp-build: 4.6.1 + dev: false + + /@datadog/native-iast-taint-tracking@1.1.0: + resolution: {integrity: sha512-TOrngpt6Qh52zWFOz1CkFXw0g43rnuUziFBtIMUsOLGzSHr9wdnTnE6HAyuvKy3f3ecAoZESlMfilGRKP93hXQ==} + dependencies: + node-gyp-build: 3.9.0 + dev: false + + /@datadog/native-metrics@1.6.0: + resolution: {integrity: sha512-+8jBzd0nlLV+ay3Vb87DLwz8JHAS817hRhSRQ6zxhud9TyvvcNTNN+VA2sb2fe5UK4aMDvj/sGVJjEtgr4RHew==} + engines: {node: '>=12'} + requiresBuild: true + dependencies: + node-gyp-build: 3.9.0 + dev: false + + /@datadog/pprof@1.1.1: + resolution: {integrity: sha512-5lYXUpikQhrJwzODtJ7aFM0oKmPccISnTCecuWhjxIj4/7UJv0DamkLak634bgEW+kiChgkKFDapHSesuXRDXQ==} + engines: {node: '>=12'} + requiresBuild: true + dependencies: + delay: 5.0.0 + findit2: 2.2.3 + node-gyp-build: 3.9.0 + p-limit: 3.1.0 + pify: 5.0.0 + protobufjs: 7.2.5 + source-map: 0.7.4 + split: 1.0.1 + dev: false + + /@datadog/sketches-js@2.1.0: + resolution: {integrity: sha512-smLocSfrt3s53H/XSVP3/1kP42oqvrkjUPtyaFd1F79ux24oE31BKt+q0c6lsa6hOYrFzsIwyc5GXAI5JmfOew==} + dev: false + + /@did-plc/lib@0.0.1: + resolution: {integrity: sha512-RkY5w9DbYMco3SjeepqIiMveqz35exjlVDipCs2gz9AXF4/cp9hvmrp9zUWEw2vny+FjV8vGEN7QpaXWaO6nhg==} + dependencies: + '@atproto/common': 0.1.0 + '@atproto/crypto': 0.1.0 + '@ipld/dag-cbor': 7.0.3 + axios: 1.4.0 + multiformats: 9.9.0 + uint8arrays: 3.0.0 + zod: 3.21.4 + transitivePeerDependencies: + - debug + + /@did-plc/server@0.0.1: + resolution: {integrity: sha512-GtxxHcOrOQ6fNI1ufq3Zqjc2PtWqPZOdsuzlwtxiH9XibUGwDkb0GmaBHyU5GiOxOKZEW1GspZ8mreBA6XOlTQ==} + dependencies: + '@atproto/common': 0.1.0 + '@atproto/crypto': 0.1.0 + '@did-plc/lib': 0.0.1 + axios: 1.4.0 + cors: 2.8.5 + express: 4.18.2 + express-async-errors: 3.1.1(express@4.18.2) + http-terminator: 3.2.0 + kysely: 0.23.5 + multiformats: 9.9.0 + pg: 8.10.0 + pino: 8.15.0 + pino-http: 8.4.0 + transitivePeerDependencies: + - debug + - pg-native + - supports-color + + /@eslint/eslintrc@1.4.1: + resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.21.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@fastify/deepmerge@1.3.0: + resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} + + /@gar/promisify@1.1.3: + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + dev: true + + /@humanwhocodes/config-array@0.10.7: + resolution: {integrity: sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/gitignore-to-minimatch@1.0.2: + resolution: {integrity: sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==} + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: true + + /@ioredis/commands@1.2.0: + resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + dev: false + + /@ipld/car@3.2.3: + resolution: {integrity: sha512-pXE5mFJlXzJVaBwqAJKGlKqMmxq8H2SLEWBJgkeBDPBIN8ZbscPc3I9itkSQSlS/s6Fgx35Ri3LDTDtodQjCCQ==} + dependencies: + '@ipld/dag-cbor': 7.0.3 + multiformats: 9.9.0 + varint: 6.0.0 + dev: false + + /@ipld/dag-cbor@7.0.3: + resolution: {integrity: sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA==} + dependencies: + cborg: 1.10.2 + multiformats: 9.9.0 + + /@isaacs/ttlcache@1.4.1: + resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} + engines: {node: '>=12'} + dev: false + + /@istanbuljs/load-nyc-config@1.1.0: + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + dev: true + + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true + + /@jest/console@28.1.3: + resolution: {integrity: sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/types': 28.1.3 + '@types/node': 18.17.8 + chalk: 4.1.2 + jest-message-util: 28.1.3 + jest-util: 28.1.3 + slash: 3.0.0 + dev: true + + /@jest/core@28.1.3(ts-node@10.8.2): + resolution: {integrity: sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 28.1.3 + '@jest/reporters': 28.1.3 + '@jest/test-result': 28.1.3 + '@jest/transform': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 18.17.8 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.8.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 28.1.3 + jest-config: 28.1.3(@types/node@18.17.8)(ts-node@10.8.2) + jest-haste-map: 28.1.3 + jest-message-util: 28.1.3 + jest-regex-util: 28.0.2 + jest-resolve: 28.1.3 + jest-resolve-dependencies: 28.1.3 + jest-runner: 28.1.3 + jest-runtime: 28.1.3 + jest-snapshot: 28.1.3 + jest-util: 28.1.3 + jest-validate: 28.1.3 + jest-watcher: 28.1.3 + micromatch: 4.0.5 + pretty-format: 28.1.3 + rimraf: 3.0.2 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - supports-color + - ts-node + dev: true + + /@jest/create-cache-key-function@27.5.1: + resolution: {integrity: sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/types': 27.5.1 + dev: true + + /@jest/environment@28.1.3: + resolution: {integrity: sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/fake-timers': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 18.17.8 + jest-mock: 28.1.3 + dev: true + + /@jest/expect-utils@28.1.3: + resolution: {integrity: sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + jest-get-type: 28.0.2 + dev: true + + /@jest/expect@28.1.3: + resolution: {integrity: sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + expect: 28.1.3 + jest-snapshot: 28.1.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/fake-timers@28.1.3: + resolution: {integrity: sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/types': 28.1.3 + '@sinonjs/fake-timers': 9.1.2 + '@types/node': 18.17.8 + jest-message-util: 28.1.3 + jest-mock: 28.1.3 + jest-util: 28.1.3 + dev: true + + /@jest/globals@28.1.3: + resolution: {integrity: sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/environment': 28.1.3 + '@jest/expect': 28.1.3 + '@jest/types': 28.1.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/reporters@28.1.3: + resolution: {integrity: sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 28.1.3 + '@jest/test-result': 28.1.3 + '@jest/transform': 28.1.3 + '@jest/types': 28.1.3 + '@jridgewell/trace-mapping': 0.3.19 + '@types/node': 18.17.8 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-instrument: 5.2.1 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.6 + jest-message-util: 28.1.3 + jest-util: 28.1.3 + jest-worker: 28.1.3 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + terminal-link: 2.1.1 + v8-to-istanbul: 9.1.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/schemas@28.1.3: + resolution: {integrity: sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@sinclair/typebox': 0.24.51 + dev: true + + /@jest/source-map@28.1.2: + resolution: {integrity: sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + callsites: 3.1.0 + graceful-fs: 4.2.11 + dev: true + + /@jest/test-result@28.1.3: + resolution: {integrity: sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/console': 28.1.3 + '@jest/types': 28.1.3 + '@types/istanbul-lib-coverage': 2.0.4 + collect-v8-coverage: 1.0.2 + dev: true + + /@jest/test-sequencer@28.1.3: + resolution: {integrity: sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/test-result': 28.1.3 + graceful-fs: 4.2.11 + jest-haste-map: 28.1.3 + slash: 3.0.0 + dev: true + + /@jest/transform@28.1.3: + resolution: {integrity: sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@babel/core': 7.18.6 + '@jest/types': 28.1.3 + '@jridgewell/trace-mapping': 0.3.19 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 1.9.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 28.1.3 + jest-regex-util: 28.0.2 + jest-util: 28.1.3 + micromatch: 4.0.5 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@jest/types@27.5.1: + resolution: {integrity: sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-reports': 3.0.1 + '@types/node': 18.17.8 + '@types/yargs': 16.0.5 + chalk: 4.1.2 + dev: true + + /@jest/types@28.1.3: + resolution: {integrity: sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/schemas': 28.1.3 + '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-reports': 3.0.1 + '@types/node': 18.17.8 + '@types/yargs': 17.0.24 + chalk: 4.1.2 + dev: true + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.19 + dev: true + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.19: + resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@noble/curves@1.1.0: + resolution: {integrity: sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==} + dependencies: + '@noble/hashes': 1.3.1 + dev: false + + /@noble/hashes@1.3.1: + resolution: {integrity: sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==} + engines: {node: '>= 16'} + dev: false + + /@noble/secp256k1@1.7.1: + resolution: {integrity: sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==} + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + + /@npmcli/fs@2.1.2: + resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.5.4 + dev: true + + /@npmcli/move-file@2.0.1: + resolution: {integrity: sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This functionality has been moved to @npmcli/fs + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + dev: true + + /@npmcli/package-json@3.0.0: + resolution: {integrity: sha512-NnuPuM97xfiCpbTEJYtEuKz6CFbpUHtaT0+5via5pQeI25omvQDFbp1GcGJ/c4zvL/WX0qbde6YiLgfZbWFgvg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + json-parse-even-better-errors: 3.0.0 + dev: true + + /@protobufjs/aspromise@1.1.2: + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + dev: false + + /@protobufjs/base64@1.1.2: + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + dev: false + + /@protobufjs/codegen@2.0.4: + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + dev: false + + /@protobufjs/eventemitter@1.1.0: + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + dev: false + + /@protobufjs/fetch@1.1.0: + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + dev: false + + /@protobufjs/float@1.0.2: + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + dev: false + + /@protobufjs/inquire@1.1.0: + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + dev: false + + /@protobufjs/path@1.1.2: + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + dev: false + + /@protobufjs/pool@1.1.0: + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + dev: false + + /@protobufjs/utf8@1.1.0: + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + dev: false + + /@sinclair/typebox@0.24.51: + resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} + dev: true + + /@sinonjs/commons@1.8.6: + resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==} + dependencies: + type-detect: 4.0.8 + dev: true + + /@sinonjs/fake-timers@9.1.2: + resolution: {integrity: sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==} + dependencies: + '@sinonjs/commons': 1.8.6 + dev: true + + /@smithy/abort-controller@1.1.0: + resolution: {integrity: sha512-5imgGUlZL4dW4YWdMYAKLmal9ny/tlenM81QZY7xYyb76z9Z/QOg7oM5Ak9HQl8QfFTlGVWwcMXl+54jroRgEQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 1.2.0 + tslib: 2.6.2 + dev: false + + /@smithy/types@1.2.0: + resolution: {integrity: sha512-z1r00TvBqF3dh4aHhya7nz1HhvCg4TRmw51fjMrh5do3h+ngSstt/yKlNbHeb9QxJmFbmN8KEVSWgb1bRvfEoA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@swc/core-darwin-arm64@1.3.42: + resolution: {integrity: sha512-hM6RrZFyoCM9mX3cj/zM5oXwhAqjUdOCLXJx7KTQps7NIkv/Qjvobgvyf2gAb89j3ARNo9NdIoLjTjJ6oALtiA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@swc/core-darwin-x64@1.3.42: + resolution: {integrity: sha512-bjsWtHMb6wJK1+RGlBs2USvgZ0txlMk11y0qBLKo32gLKTqzUwRw0Fmfzuf6Ue2a/w//7eqMlPFEre4LvJajGw==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm-gnueabihf@1.3.42: + resolution: {integrity: sha512-Oe0ggMz3MyqXNfeVmY+bBTL0hFSNY3bx8dhcqsh4vXk/ZVGse94QoC4dd92LuPHmKT0x6nsUzB86x2jU9QHW5g==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm64-gnu@1.3.42: + resolution: {integrity: sha512-ZJsa8NIW1RLmmHGTJCbM7OPSbBZ9rOMrLqDtUOGrT0uoJXZnnQqolflamB5wviW0X6h3Z3/PSTNGNDCJ3u3Lqg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm64-musl@1.3.42: + resolution: {integrity: sha512-YpZwlFAfOp5vkm/uVUJX1O7N3yJDO1fDQRWqsOPPNyIJkI2ydlRQtgN6ZylC159Qv+TimfXnGTlNr7o3iBAqjg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-x64-gnu@1.3.42: + resolution: {integrity: sha512-0ccpKnsZbyHBzaQFdP8U9i29nvOfKitm6oJfdJzlqsY/jCqwvD8kv2CAKSK8WhJz//ExI2LqNrDI0yazx5j7+A==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-x64-musl@1.3.42: + resolution: {integrity: sha512-7eckRRuTZ6+3K21uyfXXgc2ZCg0mSWRRNwNT3wap2bYkKPeqTgb8pm8xYSZNEiMuDonHEat6XCCV36lFY6kOdQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-arm64-msvc@1.3.42: + resolution: {integrity: sha512-t27dJkdw0GWANdN4TV0lY/V5vTYSx5SRjyzzZolep358ueCGuN1XFf1R0JcCbd1ojosnkQg2L7A7991UjXingg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-ia32-msvc@1.3.42: + resolution: {integrity: sha512-xfpc/Zt/aMILX4IX0e3loZaFyrae37u3MJCv1gJxgqrpeLi7efIQr3AmERkTK3mxTO6R5urSliWw2W3FyZ7D3Q==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-x64-msvc@1.3.42: + resolution: {integrity: sha512-ra2K4Tu++EJLPhzZ6L8hWUsk94TdK/2UKhL9dzCBhtzKUixsGCEqhtqH1zISXNvW8qaVLFIMUP37ULe80/IJaA==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core@1.3.42: + resolution: {integrity: sha512-nVFUd5+7tGniM2cT3LXaqnu3735Cu4az8A9gAKK+8sdpASI52SWuqfDBmjFCK9xG90MiVDVp2PTZr0BWqCIzpw==} + engines: {node: '>=10'} + requiresBuild: true + optionalDependencies: + '@swc/core-darwin-arm64': 1.3.42 + '@swc/core-darwin-x64': 1.3.42 + '@swc/core-linux-arm-gnueabihf': 1.3.42 + '@swc/core-linux-arm64-gnu': 1.3.42 + '@swc/core-linux-arm64-musl': 1.3.42 + '@swc/core-linux-x64-gnu': 1.3.42 + '@swc/core-linux-x64-musl': 1.3.42 + '@swc/core-win32-arm64-msvc': 1.3.42 + '@swc/core-win32-ia32-msvc': 1.3.42 + '@swc/core-win32-x64-msvc': 1.3.42 + dev: true + + /@swc/jest@0.2.24(@swc/core@1.3.42): + resolution: {integrity: sha512-fwgxQbM1wXzyKzl1+IW0aGrRvAA8k0Y3NxFhKigbPjOJ4mCKnWEcNX9HQS3gshflcxq8YKhadabGUVfdwjCr6Q==} + engines: {npm: '>= 7.0.0'} + peerDependencies: + '@swc/core': '*' + dependencies: + '@jest/create-cache-key-function': 27.5.1 + '@swc/core': 1.3.42 + jsonc-parser: 3.2.0 + dev: true + + /@tokenizer/token@0.3.0: + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + dev: false + + /@tootallnate/once@2.0.0: + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + dev: true + + /@ts-morph/common@0.17.0: + resolution: {integrity: sha512-RMSSvSfs9kb0VzkvQ2NWobwnj7TxCA9vI/IjR9bDHqgAyVbu2T0DN4wiKVqomyDWqO7dPr/tErSfq7urQ1Q37g==} + dependencies: + fast-glob: 3.3.1 + minimatch: 5.1.6 + mkdirp: 1.0.4 + path-browserify: 1.0.1 + dev: false + + /@tsconfig/node10@1.0.9: + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + + /@types/babel__core@7.20.1: + resolution: {integrity: sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==} + dependencies: + '@babel/parser': 7.22.10 + '@babel/types': 7.22.10 + '@types/babel__generator': 7.6.4 + '@types/babel__template': 7.4.1 + '@types/babel__traverse': 7.20.1 + dev: true + + /@types/babel__generator@7.6.4: + resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} + dependencies: + '@babel/types': 7.22.10 + dev: true + + /@types/babel__template@7.4.1: + resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} + dependencies: + '@babel/parser': 7.22.10 + '@babel/types': 7.22.10 + dev: true + + /@types/babel__traverse@7.20.1: + resolution: {integrity: sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==} + dependencies: + '@babel/types': 7.22.10 + dev: true + + /@types/bn.js@5.1.1: + resolution: {integrity: sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g==} + dependencies: + '@types/node': 18.17.8 + dev: false + + /@types/body-parser@1.19.2: + resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} + dependencies: + '@types/connect': 3.4.35 + '@types/node': 18.17.8 + dev: true + + /@types/connect@3.4.35: + resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} + dependencies: + '@types/node': 18.17.8 + dev: true + + /@types/cors@2.8.12: + resolution: {integrity: sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==} + dev: true + + /@types/elliptic@6.4.14: + resolution: {integrity: sha512-z4OBcDAU0GVwDTuwJzQCiL6188QvZMkvoERgcVjq0/mPM8jCfdwZ3x5zQEVoL9WCAru3aG5wl3Z5Ww5wBWn7ZQ==} + dependencies: + '@types/bn.js': 5.1.1 + dev: false + + /@types/express-serve-static-core@4.17.36: + resolution: {integrity: sha512-zbivROJ0ZqLAtMzgzIUC4oNqDG9iF0lSsAqpOD9kbs5xcIM3dTiyuHvBc7R8MtWBp3AAWGaovJa+wzWPjLYW7Q==} + dependencies: + '@types/node': 18.17.8 + '@types/qs': 6.9.7 + '@types/range-parser': 1.2.4 + '@types/send': 0.17.1 + dev: true + + /@types/express@4.17.13: + resolution: {integrity: sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==} + dependencies: + '@types/body-parser': 1.19.2 + '@types/express-serve-static-core': 4.17.36 + '@types/qs': 6.9.7 + '@types/serve-static': 1.15.2 + dev: true + + /@types/graceful-fs@4.1.6: + resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} + dependencies: + '@types/node': 18.17.8 + dev: true + + /@types/http-errors@2.0.1: + resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==} + dev: true + + /@types/istanbul-lib-coverage@2.0.4: + resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} + dev: true + + /@types/istanbul-lib-report@3.0.0: + resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.4 + dev: true + + /@types/istanbul-reports@3.0.1: + resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} + dependencies: + '@types/istanbul-lib-report': 3.0.0 + dev: true + + /@types/jest@28.1.4: + resolution: {integrity: sha512-telv6G5N7zRJiLcI3Rs3o+ipZ28EnE+7EvF0pSrt2pZOMnAVI/f+6/LucDxOvcBcTeTL3JMF744BbVQAVBUQRA==} + dependencies: + jest-matcher-utils: 28.1.3 + pretty-format: 28.1.3 + dev: true + + /@types/json-schema@7.0.12: + resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} + dev: true + + /@types/jsonwebtoken@8.5.9: + resolution: {integrity: sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==} + dependencies: + '@types/node': 18.17.8 + dev: true + + /@types/mime@1.3.2: + resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} + dev: true + + /@types/mime@3.0.1: + resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} + dev: true + + /@types/node@18.0.0: + resolution: {integrity: sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==} + dev: true + + /@types/node@18.17.8: + resolution: {integrity: sha512-Av/7MqX/iNKwT9Tr60V85NqMnsmh8ilfJoBlIVibkXfitk9Q22D9Y5mSpm+FvG5DET7EbVfB40bOiLzKgYFgPw==} + + /@types/nodemailer@6.4.6: + resolution: {integrity: sha512-pD6fL5GQtUKvD2WnPmg5bC2e8kWCAPDwMPmHe/ohQbW+Dy0EcHgZ2oCSuPlWNqk74LS5BVMig1SymQbFMPPK3w==} + dependencies: + '@types/node': 18.17.8 + dev: true + + /@types/pg@8.6.6: + resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==} + dependencies: + '@types/node': 18.17.8 + pg-protocol: 1.6.0 + pg-types: 2.2.0 + dev: true + + /@types/prettier@2.7.3: + resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==} + dev: true + + /@types/qs@6.9.7: + resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} + dev: true + + /@types/range-parser@1.2.4: + resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} + dev: true + + /@types/send@0.17.1: + resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} + dependencies: + '@types/mime': 1.3.2 + '@types/node': 18.17.8 + dev: true + + /@types/serve-static@1.15.2: + resolution: {integrity: sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==} + dependencies: + '@types/http-errors': 2.0.1 + '@types/mime': 3.0.1 + '@types/node': 18.17.8 + dev: true + + /@types/sharp@0.31.0: + resolution: {integrity: sha512-nwivOU101fYInCwdDcH/0/Ru6yIRXOpORx25ynEOc6/IakuCmjOAGpaO5VfUl4QkDtUC6hj+Z2eCQvgXOioknw==} + dependencies: + '@types/node': 18.17.8 + dev: true + + /@types/stack-utils@2.0.1: + resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} + dev: true + + /@types/ws@8.5.4: + resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} + dependencies: + '@types/node': 18.17.8 + dev: true + + /@types/yargs-parser@21.0.0: + resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} + dev: true + + /@types/yargs@16.0.5: + resolution: {integrity: sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==} + dependencies: + '@types/yargs-parser': 21.0.0 + dev: true + + /@types/yargs@17.0.24: + resolution: {integrity: sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==} + dependencies: + '@types/yargs-parser': 21.0.0 + dev: true + + /@typescript-eslint/eslint-plugin@5.38.1(@typescript-eslint/parser@5.38.1)(eslint@8.24.0)(typescript@4.8.4): + resolution: {integrity: sha512-ky7EFzPhqz3XlhS7vPOoMDaQnQMn+9o5ICR9CPr/6bw8HrFkzhMSxuA3gRfiJVvs7geYrSeawGJjZoZQKCOglQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/parser': 5.38.1(eslint@8.24.0)(typescript@4.8.4) + '@typescript-eslint/scope-manager': 5.38.1 + '@typescript-eslint/type-utils': 5.38.1(eslint@8.24.0)(typescript@4.8.4) + '@typescript-eslint/utils': 5.38.1(eslint@8.24.0)(typescript@4.8.4) + debug: 4.3.4 + eslint: 8.24.0 + ignore: 5.2.4 + regexpp: 3.2.0 + semver: 7.5.4 + tsutils: 3.21.0(typescript@4.8.4) + typescript: 4.8.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@5.38.1(eslint@8.24.0)(typescript@4.8.4): + resolution: {integrity: sha512-LDqxZBVFFQnQRz9rUZJhLmox+Ep5kdUmLatLQnCRR6523YV+XhRjfYzStQ4MheFA8kMAfUlclHSbu+RKdRwQKw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.38.1 + '@typescript-eslint/types': 5.38.1 + '@typescript-eslint/typescript-estree': 5.38.1(typescript@4.8.4) + debug: 4.3.4 + eslint: 8.24.0 + typescript: 4.8.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@5.38.1: + resolution: {integrity: sha512-BfRDq5RidVU3RbqApKmS7RFMtkyWMM50qWnDAkKgQiezRtLKsoyRKIvz1Ok5ilRWeD9IuHvaidaLxvGx/2eqTQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.38.1 + '@typescript-eslint/visitor-keys': 5.38.1 + dev: true + + /@typescript-eslint/type-utils@5.38.1(eslint@8.24.0)(typescript@4.8.4): + resolution: {integrity: sha512-UU3j43TM66gYtzo15ivK2ZFoDFKKP0k03MItzLdq0zV92CeGCXRfXlfQX5ILdd4/DSpHkSjIgLLLh1NtkOJOAw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.38.1(typescript@4.8.4) + '@typescript-eslint/utils': 5.38.1(eslint@8.24.0)(typescript@4.8.4) + debug: 4.3.4 + eslint: 8.24.0 + tsutils: 3.21.0(typescript@4.8.4) + typescript: 4.8.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@5.38.1: + resolution: {integrity: sha512-QTW1iHq1Tffp9lNfbfPm4WJabbvpyaehQ0SrvVK2yfV79SytD9XDVxqiPvdrv2LK7DGSFo91TB2FgWanbJAZXg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@typescript-eslint/typescript-estree@5.38.1(typescript@4.8.4): + resolution: {integrity: sha512-99b5e/Enoe8fKMLdSuwrfH/C0EIbpUWmeEKHmQlGZb8msY33qn1KlkFww0z26o5Omx7EVjzVDCWEfrfCDHfE7g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.38.1 + '@typescript-eslint/visitor-keys': 5.38.1 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + tsutils: 3.21.0(typescript@4.8.4) + typescript: 4.8.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@5.38.1(eslint@8.24.0)(typescript@4.8.4): + resolution: {integrity: sha512-oIuUiVxPBsndrN81oP8tXnFa/+EcZ03qLqPDfSZ5xIJVm7A9V0rlkQwwBOAGtrdN70ZKDlKv+l1BeT4eSFxwXA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@types/json-schema': 7.0.12 + '@typescript-eslint/scope-manager': 5.38.1 + '@typescript-eslint/types': 5.38.1 + '@typescript-eslint/typescript-estree': 5.38.1(typescript@4.8.4) + eslint: 8.24.0 + eslint-scope: 5.1.1 + eslint-utils: 3.0.0(eslint@8.24.0) + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@5.38.1: + resolution: {integrity: sha512-bSHr1rRxXt54+j2n4k54p4fj8AHJ49VDWtjpImOpzQj4qjAiOpPni+V1Tyajh19Api1i844F757cur8wH3YvOA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.38.1 + eslint-visitor-keys: 3.4.3 + dev: true + + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: true + + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + /acorn-import-assertions@1.9.0(acorn@8.10.0): + resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + peerDependencies: + acorn: ^8 + dependencies: + acorn: 8.10.0 + dev: false + + /acorn-jsx@5.3.2(acorn@8.10.0): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.10.0 + dev: true + + /acorn-walk@8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} + engines: {node: '>= 8.0.0'} + dependencies: + humanize-ms: 1.2.1 + dev: true + + /aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + dev: true + + /ajv-formats@2.1.1(ajv@8.12.0): + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.12.0 + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + dev: true + + /are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + dev: true + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + dev: true + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /arraybuffer.prototype.slice@1.0.1: + resolution: {integrity: sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.2 + define-properties: 1.2.0 + get-intrinsic: 1.2.1 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: true + + /asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + dependencies: + bn.js: 4.12.0 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + dev: false + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + /atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: true + + /axios@0.27.2: + resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + dependencies: + follow-redirects: 1.15.2 + form-data: 4.0.0 + transitivePeerDependencies: + - debug + + /axios@1.4.0: + resolution: {integrity: sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==} + dependencies: + follow-redirects: 1.15.2 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + /babel-eslint@10.1.0(eslint@8.24.0): + resolution: {integrity: sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==} + engines: {node: '>=6'} + deprecated: babel-eslint is now @babel/eslint-parser. This package will no longer receive updates. + peerDependencies: + eslint: '>= 4.12.1' + dependencies: + '@babel/code-frame': 7.22.10 + '@babel/parser': 7.22.10 + '@babel/traverse': 7.22.10 + '@babel/types': 7.22.10 + eslint: 8.24.0 + eslint-visitor-keys: 1.3.0 + resolve: 1.22.4 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-jest@28.1.3(@babel/core@7.18.6): + resolution: {integrity: sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.18.6 + '@jest/transform': 28.1.3 + '@types/babel__core': 7.20.1 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 28.1.3(@babel/core@7.18.6) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + dependencies: + '@babel/helper-plugin-utils': 7.22.5 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-jest-hoist@28.1.3: + resolution: {integrity: sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@babel/template': 7.22.5 + '@babel/types': 7.22.10 + '@types/babel__core': 7.20.1 + '@types/babel__traverse': 7.20.1 + dev: true + + /babel-plugin-polyfill-corejs2@0.3.3(@babel/core@7.18.6): + resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.22.9 + '@babel/core': 7.18.6 + '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.18.6) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-corejs3@0.5.3(@babel/core@7.18.6): + resolution: {integrity: sha512-zKsXDh0XjnrUEW0mxIHLfjBfnXSMr5Q/goMe/fxpQnLm07mcOZiIZHBNWCMx60HmdvjxfXcalac0tfFg0wqxyw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.18.6) + core-js-compat: 3.32.1 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-regenerator@0.3.1(@babel/core@7.18.6): + resolution: {integrity: sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.18.6 + '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.18.6) + transitivePeerDependencies: + - supports-color + dev: true + + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.18.6): + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.18.6 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.18.6) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.18.6) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.18.6) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.18.6) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.18.6) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.18.6) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.18.6) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.18.6) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.18.6) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.18.6) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.18.6) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.18.6) + dev: true + + /babel-preset-jest@28.1.3(@babel/core@7.18.6): + resolution: {integrity: sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.18.6 + babel-plugin-jest-hoist: 28.1.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.18.6) + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + /better-sqlite3@7.6.2: + resolution: {integrity: sha512-S5zIU1Hink2AH4xPsN0W43T1/AJ5jrPh7Oy07ocuW/AKYYY02GWzz9NH0nbSMn/gw6fDZ5jZ1QsHt1BXAwJ6Lg==} + 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'} + + /bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + dependencies: + file-uri-to-path: 1.0.0 + dev: false + + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: false + + /bn.js@4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} + dev: false + + /body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + /boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + + /bowser@2.11.0: + resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + dev: false + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + + /brorand@1.1.0: + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + dev: false + + /browserslist@4.21.10: + resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001522 + electron-to-chromium: 1.4.499 + node-releases: 2.0.13 + update-browserslist-db: 1.0.11(browserslist@4.21.10) + dev: true + + /bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + dependencies: + node-int64: 0.4.0 + dev: true + + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /buffer-writer@2.0.0: + resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} + engines: {node: '>=4'} + + /buffer@5.6.0: + resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + /bytes@3.0.0: + resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} + engines: {node: '>= 0.8'} + dev: false + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + /cacache@16.1.3: + resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + '@npmcli/fs': 2.1.2 + '@npmcli/move-file': 2.0.1 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 8.1.0 + infer-owner: 1.0.4 + lru-cache: 7.18.3 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 9.0.1 + tar: 6.1.15 + unique-filename: 2.0.1 + transitivePeerDependencies: + - bluebird + dev: true + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: true + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + dev: true + + /caniuse-lite@1.0.30001522: + resolution: {integrity: sha512-TKiyTVZxJGhsTszLuzb+6vUZSjVOAhClszBr2Ta2k9IwtNBT/4dzmL6aywt0HCgEZlmwJzXJd8yNiob6HgwTRg==} + dev: true + + /cbor-extract@2.1.1: + resolution: {integrity: sha512-1UX977+L+zOJHsp0mWFG13GLwO6ucKgSmSW6JTl8B9GUvACvHeIVpFqhU92299Z6PfD09aTXDell5p+lp1rUFA==} + hasBin: true + requiresBuild: true + dependencies: + node-gyp-build-optional-packages: 5.0.3 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.1.1 + '@cbor-extract/cbor-extract-darwin-x64': 2.1.1 + '@cbor-extract/cbor-extract-linux-arm': 2.1.1 + '@cbor-extract/cbor-extract-linux-arm64': 2.1.1 + '@cbor-extract/cbor-extract-linux-x64': 2.1.1 + '@cbor-extract/cbor-extract-win32-x64': 2.1.1 + dev: false + optional: true + + /cbor-x@1.5.1: + resolution: {integrity: sha512-/vAkC4tiKCQCm5en4sA+mpKmjwY6Xxp1LO+BgZCNhp+Zow3pomyUHeBOK5EDp0mDaE36jw39l5eLHsoF3M1Lmg==} + optionalDependencies: + cbor-extract: 2.1.1 + dev: false + + /cborg@1.10.2: + resolution: {integrity: sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug==} + hasBin: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chalk@5.1.1: + resolution: {integrity: sha512-OItMegkSDU3P7OJRWBbNRsQsL8SzgwlIGXSZRVfHCLBYrDgzYDuozwDMwvEDpiZdjr50tdOTbTzuubirtEozsg==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: false + + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + dev: true + + /chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: false + + /chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + dev: true + + /ci-info@3.8.0: + resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} + engines: {node: '>=8'} + dev: true + + /cjs-module-lexer@1.2.3: + resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + + /clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + dev: true + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + + /cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + dev: false + + /co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + dev: true + + /code-block-writer@11.0.3: + resolution: {integrity: sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==} + dev: false + + /collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + /color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + dev: false + + /color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + dev: true + + /color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + dev: false + + /colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + dev: true + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + + /commander@9.4.0: + resolution: {integrity: sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==} + engines: {node: ^12.20.0 || >=14} + dev: false + + /common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + dev: true + + /compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /compression@1.7.4: + resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} + engines: {node: '>= 0.8.0'} + dependencies: + accepts: 1.3.8 + bytes: 3.0.0 + compressible: 2.0.18 + debug: 2.6.9 + on-headers: 1.0.2 + safe-buffer: 5.1.2 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + dev: true + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + /convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + dev: true + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + + /core-js-compat@3.32.1: + resolution: {integrity: sha512-GSvKDv4wE0bPnQtjklV101juQ85g6H3rm5PDP20mqlS5j0kXF3pP97YvAu5hl+uFHqMictp3b2VxOHljWMAtuA==} + dependencies: + browserslist: 4.21.10 + dev: true + + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + + /cross-spawn@6.0.5: + resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} + engines: {node: '>=4.8'} + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.2 + shebang-command: 1.2.0 + which: 1.3.1 + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /crypto-randomuuid@1.0.0: + resolution: {integrity: sha512-/RC5F4l1SCqD/jazwUF6+t34Cd8zTSAGZ7rvvZu1whZUhD2a5MOGKjSGowoGcpj/cbVZk1ZODIooJEQQq3nNAA==} + dev: false + + /dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + dev: true + + /dd-trace@3.13.2: + resolution: {integrity: sha512-POO9nEcAufe5pgp2xV1X3PfWip6wh+6TpEcRSlSgZJCIIMvWVCkcIVL/J2a6KAZq6V3Yjbkl8Ktfe+MOzQf5kw==} + engines: {node: '>=14'} + requiresBuild: true + dependencies: + '@datadog/native-appsec': 2.0.0 + '@datadog/native-iast-rewriter': 1.1.2 + '@datadog/native-iast-taint-tracking': 1.1.0 + '@datadog/native-metrics': 1.6.0 + '@datadog/pprof': 1.1.1 + '@datadog/sketches-js': 2.1.0 + crypto-randomuuid: 1.0.0 + diagnostics_channel: 1.1.0 + ignore: 5.2.4 + import-in-the-middle: 1.4.2 + ipaddr.js: 2.1.0 + istanbul-lib-coverage: 3.2.0 + koalas: 1.0.2 + limiter: 1.1.5 + lodash.kebabcase: 4.1.1 + lodash.pick: 4.4.0 + lodash.sortby: 4.7.0 + lodash.uniq: 4.5.0 + lru-cache: 7.18.3 + methods: 1.1.2 + module-details-from-path: 1.0.3 + node-abort-controller: 3.1.1 + opentracing: 0.14.7 + path-to-regexp: 0.1.7 + protobufjs: 7.2.5 + retry: 0.10.1 + semver: 5.7.2 + dev: false + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: false + + /dedent@0.7.0: + resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + dev: true + + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: false + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + /define-properties@1.2.0: + resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} + engines: {node: '>= 0.4'} + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + + /delay@5.0.0: + resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} + engines: {node: '>=10'} + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + /delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + dev: true + + /denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + dev: false + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + /detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + dev: false + + /detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + dev: true + + /diagnostics_channel@1.1.0: + resolution: {integrity: sha512-OE1ngLDjSBPG6Tx0YATELzYzy3RKHC+7veQ8gLa8yS7AAgw65mFbVdcsu3501abqOZCEZqZyAIemB0zXlqDSuw==} + engines: {node: '>=4'} + dev: false + + /diff-sequences@28.1.1: + resolution: {integrity: sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dev: true + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + dev: false + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: false + + /domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: false + + /domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + dev: false + + /dotenv@16.0.3: + resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} + engines: {node: '>=12'} + + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + /electron-to-chromium@1.4.499: + resolution: {integrity: sha512-0NmjlYBLKVHva4GABWAaHuPJolnDuL0AhV3h1hES6rcLCWEIbRL6/8TghfsVwkx6TEroQVdliX7+aLysUpKvjw==} + dev: true + + /elliptic@6.5.4: + resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: false + + /emittery@0.10.2: + resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==} + engines: {node: '>=12'} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + /encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + requiresBuild: true + dependencies: + iconv-lite: 0.6.3 + dev: true + optional: true + + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + + /entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + dev: false + + /env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + dev: true + + /err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + dev: true + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: true + + /es-abstract@1.22.1: + resolution: {integrity: sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.1 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.5 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.0 + safe-array-concat: 1.0.0 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.7 + string.prototype.trimend: 1.0.6 + string.prototype.trimstart: 1.0.6 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.11 + dev: true + + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + has-tostringtag: 1.0.0 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /esbuild-android-64@0.14.48: + resolution: {integrity: sha512-3aMjboap/kqwCUpGWIjsk20TtxVoKck8/4Tu19rubh7t5Ra0Yrpg30Mt1QXXlipOazrEceGeWurXKeFJgkPOUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-android-arm64@0.14.48: + resolution: {integrity: sha512-vptI3K0wGALiDq+EvRuZotZrJqkYkN5282iAfcffjI5lmGG9G1ta/CIVauhY42MBXwEgDJkweiDcDMRLzBZC4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-64@0.14.48: + resolution: {integrity: sha512-gGQZa4+hab2Va/Zww94YbshLuWteyKGD3+EsVon8EWTWhnHFRm5N9NbALNbwi/7hQ/hM1Zm4FuHg+k6BLsl5UA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-arm64@0.14.48: + resolution: {integrity: sha512-bFjnNEXjhZT+IZ8RvRGNJthLWNHV5JkCtuOFOnjvo5pC0sk2/QVk0Qc06g2PV3J0TcU6kaPC3RN9yy9w2PSLEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-64@0.14.48: + resolution: {integrity: sha512-1NOlwRxmOsnPcWOGTB10JKAkYSb2nue0oM1AfHWunW/mv3wERfJmnYlGzL3UAOIUXZqW8GeA2mv+QGwq7DToqA==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-arm64@0.14.48: + resolution: {integrity: sha512-gXqKdO8wabVcYtluAbikDH2jhXp+Klq5oCD5qbVyUG6tFiGhrC9oczKq3vIrrtwcxDQqK6+HDYK8Zrd4bCA9Gw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-32@0.14.48: + resolution: {integrity: sha512-ghGyDfS289z/LReZQUuuKq9KlTiTspxL8SITBFQFAFRA/IkIvDpnZnCAKTCjGXAmUqroMQfKJXMxyjJA69c/nQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-64@0.14.48: + resolution: {integrity: sha512-vni3p/gppLMVZLghI7oMqbOZdGmLbbKR23XFARKnszCIBpEMEDxOMNIKPmMItQrmH/iJrL1z8Jt2nynY0bE1ug==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm64@0.14.48: + resolution: {integrity: sha512-3CFsOlpoxlKPRevEHq8aAntgYGYkE1N9yRYAcPyng/p4Wyx0tPR5SBYsxLKcgPB9mR8chHEhtWYz6EZ+H199Zw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm@0.14.48: + resolution: {integrity: sha512-+VfSV7Akh1XUiDNXgqgY1cUP1i2vjI+BmlyXRfVz5AfV3jbpde8JTs5Q9sYgaoq5cWfuKfoZB/QkGOI+QcL1Tw==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-mips64le@0.14.48: + resolution: {integrity: sha512-cs0uOiRlPp6ymknDnjajCgvDMSsLw5mST2UXh+ZIrXTj2Ifyf2aAP3Iw4DiqgnyYLV2O/v/yWBJx+WfmKEpNLA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-ppc64le@0.14.48: + resolution: {integrity: sha512-+2F0vJMkuI0Wie/wcSPDCqXvSFEELH7Jubxb7mpWrA/4NpT+/byjxDz0gG6R1WJoeDefcrMfpBx4GFNN1JQorQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-riscv64@0.14.48: + resolution: {integrity: sha512-BmaK/GfEE+5F2/QDrIXteFGKnVHGxlnK9MjdVKMTfvtmudjY3k2t8NtlY4qemKSizc+QwyombGWTBDc76rxePA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-s390x@0.14.48: + resolution: {integrity: sha512-tndw/0B9jiCL+KWKo0TSMaUm5UWBLsfCKVdbfMlb3d5LeV9WbijZ8Ordia8SAYv38VSJWOEt6eDCdOx8LqkC4g==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-netbsd-64@0.14.48: + resolution: {integrity: sha512-V9hgXfwf/T901Lr1wkOfoevtyNkrxmMcRHyticybBUHookznipMOHoF41Al68QBsqBxnITCEpjjd4yAos7z9Tw==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-node-externals@1.5.0(esbuild@0.14.48): + resolution: {integrity: sha512-9394Ne2t2Z243BWeNBRkXEYVMOVbQuzp7XSkASZTOQs0GSXDuno5aH5OmzEXc6GMuln5zJjpkZpgwUPW0uRKgw==} + peerDependencies: + esbuild: 0.12 - 0.15 + dependencies: + esbuild: 0.14.48 + find-up: 5.0.0 + tslib: 2.3.1 + dev: true + + /esbuild-openbsd-64@0.14.48: + resolution: {integrity: sha512-+IHf4JcbnnBl4T52egorXMatil/za0awqzg2Vy6FBgPcBpisDWT2sVz/tNdrK9kAqj+GZG/jZdrOkj7wsrNTKA==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-plugin-copy@1.6.0(esbuild@0.14.48): + resolution: {integrity: sha512-wN1paBCoE0yRBl9ZY3ZSD6SxGE4Yfr0Em7zh2yTbJv1JaHEIR3FYYN7HU6F+j/peSaGZJNSORSGxJ5QX1a1Sgg==} + peerDependencies: + esbuild: '>= 0.14.0' + dependencies: + chalk: 4.1.2 + esbuild: 0.14.48 + fs-extra: 10.1.0 + globby: 11.1.0 + dev: true + + /esbuild-sunos-64@0.14.48: + resolution: {integrity: sha512-77m8bsr5wOpOWbGi9KSqDphcq6dFeJyun8TA+12JW/GAjyfTwVtOnN8DOt6DSPUfEV+ltVMNqtXUeTeMAxl5KA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-32@0.14.48: + resolution: {integrity: sha512-EPgRuTPP8vK9maxpTGDe5lSoIBHGKO/AuxDncg5O3NkrPeLNdvvK8oywB0zGaAZXxYWfNNSHskvvDgmfVTguhg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-64@0.14.48: + resolution: {integrity: sha512-YmpXjdT1q0b8ictSdGwH3M8VCoqPpK1/UArze3X199w6u8hUx3V8BhAi1WjbsfDYRBanVVtduAhh2sirImtAvA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-arm64@0.14.48: + resolution: {integrity: sha512-HHaOMCsCXp0rz5BT2crTka6MPWVno121NKApsGs/OIW5QC0ggC69YMGs1aJct9/9FSUF4A1xNE/cLvgB5svR4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild@0.14.48: + resolution: {integrity: sha512-w6N1Yn5MtqK2U1/WZTX9ZqUVb8IOLZkZ5AdHkT6x3cHDMVsYWC7WPdiLmx19w3i4Rwzy5LqsEMtVihG3e4rFzA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + esbuild-android-64: 0.14.48 + esbuild-android-arm64: 0.14.48 + esbuild-darwin-64: 0.14.48 + esbuild-darwin-arm64: 0.14.48 + esbuild-freebsd-64: 0.14.48 + esbuild-freebsd-arm64: 0.14.48 + esbuild-linux-32: 0.14.48 + esbuild-linux-64: 0.14.48 + esbuild-linux-arm: 0.14.48 + esbuild-linux-arm64: 0.14.48 + esbuild-linux-mips64le: 0.14.48 + esbuild-linux-ppc64le: 0.14.48 + esbuild-linux-riscv64: 0.14.48 + esbuild-linux-s390x: 0.14.48 + esbuild-netbsd-64: 0.14.48 + esbuild-openbsd-64: 0.14.48 + esbuild-sunos-64: 0.14.48 + esbuild-windows-32: 0.14.48 + esbuild-windows-64: 0.14.48 + esbuild-windows-arm64: 0.14.48 + dev: true + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-config-prettier@8.5.0(eslint@8.24.0): + resolution: {integrity: sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.24.0 + dev: true + + /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.5.0)(eslint@8.24.0)(prettier@2.7.1): + resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + eslint: '>=7.28.0' + eslint-config-prettier: '*' + prettier: '>=2.0.0' + peerDependenciesMeta: + eslint-config-prettier: + optional: true + dependencies: + eslint: 8.24.0 + eslint-config-prettier: 8.5.0(eslint@8.24.0) + prettier: 2.7.1 + prettier-linter-helpers: 1.0.0 + dev: true + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-utils@3.0.0(eslint@8.24.0): + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + dependencies: + eslint: 8.24.0 + eslint-visitor-keys: 2.1.0 + dev: true + + /eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + dev: true + + /eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.24.0: + resolution: {integrity: sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint/eslintrc': 1.4.1 + '@humanwhocodes/config-array': 0.10.7 + '@humanwhocodes/gitignore-to-minimatch': 1.0.2 + '@humanwhocodes/module-importer': 1.0.1 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-utils: 3.0.0(eslint@8.24.0) + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.21.0 + globby: 11.1.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.4 + import-fresh: 3.3.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + js-sdsl: 4.4.2 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + regexpp: 3.2.0 + strip-ansi: 6.0.1 + strip-json-comments: 3.1.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) + eslint-visitor-keys: 3.4.3 + dev: true + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + /eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: false + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + dev: true + + /expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + dev: false + + /expect@28.1.3: + resolution: {integrity: sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/expect-utils': 28.1.3 + jest-get-type: 28.0.2 + jest-matcher-utils: 28.1.3 + jest-message-util: 28.1.3 + jest-util: 28.1.3 + dev: true + + /express-async-errors@3.1.1(express@4.18.2): + resolution: {integrity: sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==} + peerDependencies: + express: ^4.16.2 + dependencies: + express: 4.18.2 + + /express@4.18.2: + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + /fast-copy@2.1.7: + resolution: {integrity: sha512-ozrGwyuCTAy7YgFCua8rmqmytECYk/JYAMXcswOcm0qvGoE3tPb7ivBeIHTOK2DiapBhDZgacIhzhQIKU5TCfA==} + dev: true + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + /fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + dev: true + + /fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-json-stringify@5.8.0: + resolution: {integrity: sha512-VVwK8CFMSALIvt14U8AvrSzQAwN/0vaVRiFFUVlpnXSnDGrSkOAO5MtzyN8oQNjLd5AqTW5OZRgyjoNuAuR3jQ==} + dependencies: + '@fastify/deepmerge': 1.3.0 + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + fast-deep-equal: 3.1.3 + fast-uri: 2.2.0 + rfdc: 1.3.0 + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fast-printf@1.6.9: + resolution: {integrity: sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==} + engines: {node: '>=10.0'} + dependencies: + boolean: 3.2.0 + + /fast-redact@3.3.0: + resolution: {integrity: sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==} + engines: {node: '>=6'} + + /fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + dev: true + + /fast-uri@2.2.0: + resolution: {integrity: sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==} + + /fast-url-parser@1.1.3: + 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==} + hasBin: true + dependencies: + strnum: 1.0.5 + dev: false + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + + /fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + dependencies: + bser: 2.1.1 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.0.4 + dev: true + + /file-type@16.5.4: + resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} + engines: {node: '>=10'} + dependencies: + readable-web-to-node-stream: 3.0.2 + strtok3: 6.3.0 + token-types: 4.2.1 + dev: false + + /file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + dev: false + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + + /finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /findit2@2.2.3: + resolution: {integrity: sha512-lg/Moejf4qXovVutL0Lz4IsaPoNYMuxt4PA0nGqFxnJ1CTTGGlEO2wKgoDpwknhvZ8k4Q2F+eesgkLbG2Mxfog==} + engines: {node: '>=0.8.22'} + dev: false + + /flat-cache@3.0.4: + resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.7 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.7: + resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} + dev: true + + /follow-redirects@1.15.2: + resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + /fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: false + + /fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: true + + /fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + + /function.prototype.name@1.1.5: + resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + + /gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + dev: true + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-proto: 1.0.1 + has-symbols: 1.0.3 + + /get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + dev: true + + /get-port@6.1.2: + resolution: {integrity: sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: true + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: true + + /github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + dev: false + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + dev: true + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globals@13.21.0: + resolution: {integrity: sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.0 + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.1 + ignore: 5.2.4 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: true + + /grapheme-splitter@1.0.4: + resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} + dev: true + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: false + + /handlebars@4.7.7: + resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} + engines: {node: '>=0.4.7'} + hasBin: true + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.17.4 + dev: false + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + dev: true + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + + /hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + dev: false + + /he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: false + + /help-me@4.2.0: + resolution: {integrity: sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==} + dependencies: + glob: 8.1.0 + readable-stream: 3.6.2 + dev: true + + /hmac-drbg@1.0.1: + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + dependencies: + hash.js: 1.1.7 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: false + + /hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + dev: true + + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true + + /html-to-text@7.1.1: + resolution: {integrity: sha512-c9QWysrfnRZevVpS8MlE7PyOdSuIOjg8Bt8ZE10jMU/BEngA6j3llj4GRfAmtQzcd1FjKE0sWu5IHXRUH9YxIQ==} + engines: {node: '>=10.23.2'} + hasBin: true + dependencies: + deepmerge: 4.3.1 + he: 1.2.0 + htmlparser2: 6.1.0 + minimist: 1.2.8 + dev: false + + /htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 2.2.0 + dev: false + + /http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + dev: true + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + /http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /http-terminator@3.2.0: + resolution: {integrity: sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g==} + engines: {node: '>=14'} + dependencies: + delay: 5.0.0 + p-wait-for: 3.2.0 + roarr: 7.15.1 + type-fest: 2.19.0 + + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: true + + /humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + dependencies: + ms: 2.1.3 + dev: true + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + requiresBuild: true + dependencies: + safer-buffer: 2.1.2 + dev: true + optional: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /import-in-the-middle@1.4.2: + resolution: {integrity: sha512-9WOz1Yh/cvO/p69sxRmhyQwrIGGSp7EIdcb+fFNVi7CzQGQB8U1/1XrKVSbEd/GNOAeM0peJtmi7+qphe7NvAw==} + dependencies: + acorn: 8.10.0 + acorn-import-assertions: 1.9.0(acorn@8.10.0) + cjs-module-lexer: 1.2.3 + module-details-from-path: 1.0.3 + dev: false + + /import-local@3.1.0: + resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} + engines: {node: '>=8'} + hasBin: true + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true + + /infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: false + + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + side-channel: 1.0.4 + dev: true + + /ioredis@5.3.2: + resolution: {integrity: sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==} + engines: {node: '>=12.22.0'} + dependencies: + '@ioredis/commands': 1.2.0 + cluster-key-slot: 1.1.2 + debug: 4.3.4 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + + /ip@2.0.0: + resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} + dev: true + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + /ipaddr.js@2.1.0: + resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==} + engines: {node: '>= 10'} + dev: false + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: true + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true + + /is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + dev: false + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + dependencies: + has: 1.0.3 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + + /is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + dev: true + + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: true + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: true + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.11 + dev: true + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: true + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /iso-datestring-validator@2.2.2: + resolution: {integrity: sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==} + dev: false + + /istanbul-lib-coverage@3.2.0: + resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} + engines: {node: '>=8'} + + /istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + dependencies: + '@babel/core': 7.18.6 + '@babel/parser': 7.22.10 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.0 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.3.4 + istanbul-lib-coverage: 3.2.0 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-reports@3.1.6: + resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true + + /jest-changed-files@28.1.3: + resolution: {integrity: sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + execa: 5.1.1 + p-limit: 3.1.0 + dev: true + + /jest-circus@28.1.3: + resolution: {integrity: sha512-cZ+eS5zc79MBwt+IhQhiEp0OeBddpc1n8MBo1nMB8A7oPMKEO+Sre+wHaLJexQUj9Ya/8NOBY0RESUgYjB6fow==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/environment': 28.1.3 + '@jest/expect': 28.1.3 + '@jest/test-result': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 18.17.8 + chalk: 4.1.2 + co: 4.6.0 + dedent: 0.7.0 + is-generator-fn: 2.1.0 + jest-each: 28.1.3 + jest-matcher-utils: 28.1.3 + jest-message-util: 28.1.3 + jest-runtime: 28.1.3 + jest-snapshot: 28.1.3 + jest-util: 28.1.3 + p-limit: 3.1.0 + pretty-format: 28.1.3 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-cli@28.1.3(@types/node@18.0.0)(ts-node@10.8.2): + resolution: {integrity: sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 28.1.3(ts-node@10.8.2) + '@jest/test-result': 28.1.3 + '@jest/types': 28.1.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + import-local: 3.1.0 + jest-config: 28.1.3(@types/node@18.0.0)(ts-node@10.8.2) + jest-util: 28.1.3 + jest-validate: 28.1.3 + prompts: 2.4.2 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node + dev: true + + /jest-config@28.1.3(@types/node@18.0.0)(ts-node@10.8.2): + resolution: {integrity: sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.18.6 + '@jest/test-sequencer': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 18.0.0 + babel-jest: 28.1.3(@babel/core@7.18.6) + chalk: 4.1.2 + ci-info: 3.8.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 28.1.3 + jest-environment-node: 28.1.3 + jest-get-type: 28.0.2 + jest-regex-util: 28.0.2 + jest-resolve: 28.1.3 + jest-runner: 28.1.3 + jest-util: 28.1.3 + jest-validate: 28.1.3 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 28.1.3 + slash: 3.0.0 + strip-json-comments: 3.1.1 + ts-node: 10.8.2(@swc/core@1.3.42)(@types/node@18.0.0)(typescript@4.8.4) + transitivePeerDependencies: + - supports-color + dev: true + + /jest-config@28.1.3(@types/node@18.17.8)(ts-node@10.8.2): + resolution: {integrity: sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.18.6 + '@jest/test-sequencer': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 18.17.8 + babel-jest: 28.1.3(@babel/core@7.18.6) + chalk: 4.1.2 + ci-info: 3.8.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 28.1.3 + jest-environment-node: 28.1.3 + jest-get-type: 28.0.2 + jest-regex-util: 28.0.2 + jest-resolve: 28.1.3 + jest-runner: 28.1.3 + jest-util: 28.1.3 + jest-validate: 28.1.3 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 28.1.3 + slash: 3.0.0 + strip-json-comments: 3.1.1 + ts-node: 10.8.2(@swc/core@1.3.42)(@types/node@18.0.0)(typescript@4.8.4) + transitivePeerDependencies: + - supports-color + dev: true + + /jest-diff@28.1.3: + resolution: {integrity: sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 28.1.1 + jest-get-type: 28.0.2 + pretty-format: 28.1.3 + dev: true + + /jest-docblock@28.1.1: + resolution: {integrity: sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + detect-newline: 3.1.0 + dev: true + + /jest-each@28.1.3: + resolution: {integrity: sha512-arT1z4sg2yABU5uogObVPvSlSMQlDA48owx07BDPAiasW0yYpYHYOo4HHLz9q0BVzDVU4hILFjzJw0So9aCL/g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/types': 28.1.3 + chalk: 4.1.2 + jest-get-type: 28.0.2 + jest-util: 28.1.3 + pretty-format: 28.1.3 + dev: true + + /jest-environment-node@28.1.3: + resolution: {integrity: sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/environment': 28.1.3 + '@jest/fake-timers': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 18.17.8 + jest-mock: 28.1.3 + jest-util: 28.1.3 + dev: true + + /jest-get-type@28.0.2: + resolution: {integrity: sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dev: true + + /jest-haste-map@28.1.3: + resolution: {integrity: sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/types': 28.1.3 + '@types/graceful-fs': 4.1.6 + '@types/node': 18.17.8 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 28.0.2 + jest-util: 28.1.3 + jest-worker: 28.1.3 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /jest-leak-detector@28.1.3: + resolution: {integrity: sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + jest-get-type: 28.0.2 + pretty-format: 28.1.3 + dev: true + + /jest-matcher-utils@28.1.3: + resolution: {integrity: sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 28.1.3 + jest-get-type: 28.0.2 + pretty-format: 28.1.3 + dev: true + + /jest-message-util@28.1.3: + resolution: {integrity: sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@babel/code-frame': 7.22.10 + '@jest/types': 28.1.3 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 28.1.3 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: true + + /jest-mock@28.1.3: + resolution: {integrity: sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/types': 28.1.3 + '@types/node': 18.17.8 + dev: true + + /jest-pnp-resolver@1.2.3(jest-resolve@28.1.3): + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 28.1.3 + dev: true + + /jest-regex-util@28.0.2: + resolution: {integrity: sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dev: true + + /jest-resolve-dependencies@28.1.3: + resolution: {integrity: sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + jest-regex-util: 28.0.2 + jest-snapshot: 28.1.3 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-resolve@28.1.3: + resolution: {integrity: sha512-Z1W3tTjE6QaNI90qo/BJpfnvpxtaFTFw5CDgwpyE/Kz8U/06N1Hjf4ia9quUhCh39qIGWF1ZuxFiBiJQwSEYKQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 28.1.3 + jest-pnp-resolver: 1.2.3(jest-resolve@28.1.3) + jest-util: 28.1.3 + jest-validate: 28.1.3 + resolve: 1.22.4 + resolve.exports: 1.1.1 + slash: 3.0.0 + dev: true + + /jest-runner@28.1.3: + resolution: {integrity: sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/console': 28.1.3 + '@jest/environment': 28.1.3 + '@jest/test-result': 28.1.3 + '@jest/transform': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 18.17.8 + chalk: 4.1.2 + emittery: 0.10.2 + graceful-fs: 4.2.11 + jest-docblock: 28.1.1 + jest-environment-node: 28.1.3 + jest-haste-map: 28.1.3 + jest-leak-detector: 28.1.3 + jest-message-util: 28.1.3 + jest-resolve: 28.1.3 + jest-runtime: 28.1.3 + jest-util: 28.1.3 + jest-watcher: 28.1.3 + jest-worker: 28.1.3 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-runtime@28.1.3: + resolution: {integrity: sha512-NU+881ScBQQLc1JHG5eJGU7Ui3kLKrmwCPPtYsJtBykixrM2OhVQlpMmFWJjMyDfdkGgBMNjXCGB/ebzsgNGQw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/environment': 28.1.3 + '@jest/fake-timers': 28.1.3 + '@jest/globals': 28.1.3 + '@jest/source-map': 28.1.2 + '@jest/test-result': 28.1.3 + '@jest/transform': 28.1.3 + '@jest/types': 28.1.3 + chalk: 4.1.2 + cjs-module-lexer: 1.2.3 + collect-v8-coverage: 1.0.2 + execa: 5.1.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 28.1.3 + jest-message-util: 28.1.3 + jest-mock: 28.1.3 + jest-regex-util: 28.0.2 + jest-resolve: 28.1.3 + jest-snapshot: 28.1.3 + jest-util: 28.1.3 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-snapshot@28.1.3: + resolution: {integrity: sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@babel/core': 7.18.6 + '@babel/generator': 7.22.10 + '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.18.6) + '@babel/traverse': 7.22.10 + '@babel/types': 7.22.10 + '@jest/expect-utils': 28.1.3 + '@jest/transform': 28.1.3 + '@jest/types': 28.1.3 + '@types/babel__traverse': 7.20.1 + '@types/prettier': 2.7.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.18.6) + chalk: 4.1.2 + expect: 28.1.3 + graceful-fs: 4.2.11 + jest-diff: 28.1.3 + jest-get-type: 28.0.2 + jest-haste-map: 28.1.3 + jest-matcher-utils: 28.1.3 + jest-message-util: 28.1.3 + jest-util: 28.1.3 + natural-compare: 1.4.0 + pretty-format: 28.1.3 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + + /jest-util@28.1.3: + resolution: {integrity: sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/types': 28.1.3 + '@types/node': 18.17.8 + chalk: 4.1.2 + ci-info: 3.8.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + dev: true + + /jest-validate@28.1.3: + resolution: {integrity: sha512-SZbOGBWEsaTxBGCOpsRWlXlvNkvTkY0XxRfh7zYmvd8uL5Qzyg0CHAXiXKROflh801quA6+/DsT4ODDthOC/OA==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/types': 28.1.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 28.0.2 + leven: 3.1.0 + pretty-format: 28.1.3 + dev: true + + /jest-watcher@28.1.3: + resolution: {integrity: sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/test-result': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 18.17.8 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.10.2 + jest-util: 28.1.3 + string-length: 4.0.2 + dev: true + + /jest-worker@28.1.3: + resolution: {integrity: sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@types/node': 18.17.8 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + + /jest@28.1.2(@types/node@18.0.0)(ts-node@10.8.2): + resolution: {integrity: sha512-Tuf05DwLeCh2cfWCQbcz9UxldoDyiR1E9Igaei5khjonKncYdc6LDfynKCEWozK0oLE3GD+xKAo2u8x/0s6GOg==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 28.1.3(ts-node@10.8.2) + '@jest/types': 28.1.3 + import-local: 3.1.0 + jest-cli: 28.1.3(@types/node@18.0.0)(ts-node@10.8.2) + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node + dev: true + + /joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + dev: true + + /js-sdsl@4.4.2: + resolution: {integrity: sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w==} + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: true + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + + /json-parse-even-better-errors@3.0.0: + resolution: {integrity: sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /jsonwebtoken@8.5.1: + resolution: {integrity: sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==} + engines: {node: '>=4', npm: '>=1.4.28'} + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 5.7.2 + dev: false + + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + + /key-encoder@2.0.3: + resolution: {integrity: sha512-fgBtpAGIr/Fy5/+ZLQZIPPhsZEcbSlYu/Wu96tNDFNSjSACw5lEIOFeaVdQ/iwrb8oxjlWi6wmWdH76hV6GZjg==} + dependencies: + '@types/elliptic': 6.4.14 + asn1.js: 5.4.1 + bn.js: 4.12.0 + elliptic: 6.5.4 + dev: false + + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + dev: true + + /koalas@1.0.2: + resolution: {integrity: sha512-RYhBbYaTTTHId3l6fnMZc3eGQNW6FVCqMG6AMwA5I1Mafr6AflaXeoi6x3xQuATRotGYRLk6+1ELZH4dstFNOA==} + engines: {node: '>=0.10.0'} + dev: false + + /kysely@0.22.0: + resolution: {integrity: sha512-ZE3qWtnqLOalodzfK5QUEcm7AEulhxsPNuKaGFsC3XiqO92vMLm+mAHk/NnbSIOtC4RmGm0nsv700i8KDp1gfQ==} + engines: {node: '>=14.0.0'} + dev: false + + /kysely@0.23.5: + resolution: {integrity: sha512-TH+b56pVXQq0tsyooYLeNfV11j6ih7D50dyN8tkM0e7ndiUH28Nziojiog3qRFlmEj9XePYdZUrNJ2079Qjdow==} + engines: {node: '>=14.0.0'} + + /leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /limiter@1.1.5: + resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} + dev: false + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /load-json-file@4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + dependencies: + graceful-fs: 4.2.11 + parse-json: 4.0.0 + pify: 3.0.0 + strip-bom: 3.0.0 + dev: true + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + dev: true + + /lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + dev: false + + /lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + dev: false + + /lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + dev: false + + /lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + dev: false + + /lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + dev: false + + /lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + dev: false + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: false + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false + + /lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + dev: false + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: false + + /lodash.pick@4.4.0: + resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==} + dev: false + + /lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + dev: false + + /lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + dev: false + + /long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + dev: false + + /lru-cache@10.0.1: + resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} + engines: {node: 14 || >=16.14} + dev: false + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + + /lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + dev: true + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + + /make-fetch-happen@10.2.1: + resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + agentkeepalive: 4.5.0 + cacache: 16.1.3 + http-cache-semantics: 4.1.1 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 7.18.3 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 2.1.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.3 + promise-retry: 2.0.1 + socks-proxy-agent: 7.0.0 + ssri: 9.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + dev: true + + /makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + dependencies: + tmpl: 1.0.5 + dev: true + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + /memorystream@0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + dev: true + + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: true + + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: false + + /minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + dev: false + + /minimalistic-crypto-utils@1.0.1: + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + /minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + dev: true + + /minipass-fetch@2.1.2: + resolution: {integrity: sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + dev: true + + /minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + dev: true + + /minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + dependencies: + minipass: 3.3.6 + dev: true + + /minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + dependencies: + minipass: 3.3.6 + dev: true + + /minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + dependencies: + yallist: 4.0.0 + dev: true + + /minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + dev: true + + /minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + dev: true + + /mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: false + + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + /module-details-from-path@1.0.3: + resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} + dev: false + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + /multiformats@9.9.0: + resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} + + /napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + dev: false + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: false + + /nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + dev: true + + /node-abi@3.47.0: + resolution: {integrity: sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + dev: false + + /node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + dev: false + + /node-addon-api@5.1.0: + resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + dev: false + + /node-gyp-build-optional-packages@5.0.3: + resolution: {integrity: sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==} + hasBin: true + requiresBuild: true + dev: false + optional: true + + /node-gyp-build@3.9.0: + resolution: {integrity: sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A==} + hasBin: true + dev: false + + /node-gyp-build@4.6.1: + resolution: {integrity: sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==} + hasBin: true + dev: false + + /node-gyp@9.3.1: + resolution: {integrity: sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg==} + engines: {node: ^12.13 || ^14.13 || >=16} + hasBin: true + dependencies: + env-paths: 2.2.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + make-fetch-happen: 10.2.1 + nopt: 6.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.5.4 + tar: 6.1.15 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + dev: true + + /node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + dev: true + + /node-releases@2.0.13: + resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} + dev: true + + /nodemailer-html-to-text@3.2.0: + resolution: {integrity: sha512-RJUC6640QV1PzTHHapOrc6IzrAJUZtk2BdVdINZ9VTLm+mcQNyBO9LYyhrnufkzqiD9l8hPLJ97rSyK4WanPNg==} + engines: {node: '>= 10.23.0'} + dependencies: + html-to-text: 7.1.1 + dev: false + + /nodemailer@6.8.0: + resolution: {integrity: sha512-EjYvSmHzekz6VNkNd12aUqAco+bOkRe3Of5jVhltqKhEsjw/y0PYPJfp83+s9Wzh1dspYAkUW/YNQ350NATbSQ==} + engines: {node: '>=6.0.0'} + dev: false + + /nopt@6.0.0: + resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: true + + /normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.4 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /npm-run-all@4.1.5: + resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==} + engines: {node: '>= 4'} + hasBin: true + dependencies: + ansi-styles: 3.2.1 + chalk: 2.4.2 + cross-spawn: 6.0.5 + memorystream: 0.3.1 + minimatch: 3.1.2 + pidtree: 0.3.1 + read-pkg: 3.0.0 + shell-quote: 1.8.1 + string.prototype.padend: 3.1.4 + dev: true + + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + dev: true + + /npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /on-exit-leak-free@2.1.0: + resolution: {integrity: sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==} + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + + /on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + dev: false + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + + /one-webcrypto@1.0.3: + resolution: {integrity: sha512-fu9ywBVBPx0gS9K0etIROTiCkvI5S1TDjFsYFb3rC1ewFxeOqsbzq7aIMBHsYfrTHBcGXJaONXXjTl8B01cW1Q==} + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: true + + /opentracing@0.14.7: + resolution: {integrity: sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q==} + engines: {node: '>=0.10'} + dev: false + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + dependencies: + aggregate-error: 3.1.0 + dev: true + + /p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + dev: false + + /p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + dependencies: + p-finally: 1.0.0 + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + + /p-wait-for@3.2.0: + resolution: {integrity: sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==} + engines: {node: '>=8'} + dependencies: + p-timeout: 3.2.0 + + /packet-reader@1.0.0: + resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + dependencies: + error-ex: 1.3.2 + json-parse-better-errors: 1.0.2 + dev: true + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.22.10 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: true + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + /path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + dev: false + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + + /path-type@3.0.0: + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} + engines: {node: '>=4'} + dependencies: + pify: 3.0.0 + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /peek-readable@4.1.0: + resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} + engines: {node: '>=8'} + dev: false + + /pg-connection-string@2.6.2: + resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} + + /pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + /pg-pool@3.6.1(pg@8.10.0): + resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==} + peerDependencies: + pg: '>=8.0' + dependencies: + pg: 8.10.0 + + /pg-protocol@1.6.0: + resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} + + /pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + /pg@8.10.0: + resolution: {integrity: sha512-ke7o7qSTMb47iwzOSaZMfeR7xToFdkE71ifIipOAAaLIM0DYzfOAXlgFFmYUIE2BcJtvnVlGCID84ZzCegE8CQ==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + dependencies: + buffer-writer: 2.0.0 + packet-reader: 1.0.0 + pg-connection-string: 2.6.2 + pg-pool: 3.6.1(pg@8.10.0) + pg-protocol: 1.6.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + + /pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + dependencies: + split2: 4.2.0 + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + /pidtree@0.3.1: + resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==} + engines: {node: '>=0.10'} + hasBin: true + dev: true + + /pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + dev: true + + /pify@5.0.0: + resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} + engines: {node: '>=10'} + dev: false + + /pino-abstract-transport@1.0.0: + resolution: {integrity: sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==} + dependencies: + readable-stream: 4.4.2 + split2: 4.2.0 + + /pino-http@8.2.1: + resolution: {integrity: sha512-bdWAE4HYfFjDhKw2/N7BLNSIFAs+WDLZnetsGRpBdNEKq7/RoZUgblLS5OlMY257RPQml6J5QiiLkwxbstzWbA==} + dependencies: + fast-url-parser: 1.1.3 + get-caller-file: 2.0.5 + 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==} + dependencies: + get-caller-file: 2.0.5 + pino: 8.15.0 + pino-std-serializers: 6.2.2 + process-warning: 2.2.0 + + /pino-pretty@9.1.0: + resolution: {integrity: sha512-IM6NY9LLo/dVgY7/prJhCh4rAJukafdt0ibxeNOWc2fxKMyTk90SOB9Ao2HfbtShT9QPeP0ePpJktksMhSQMYA==} + hasBin: true + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 2.1.7 + fast-safe-stringify: 2.1.1 + help-me: 4.2.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.0 + pino-abstract-transport: 1.0.0 + pump: 3.0.0 + readable-stream: 4.4.2 + secure-json-parse: 2.7.0 + sonic-boom: 3.3.0 + strip-json-comments: 3.1.1 + dev: true + + /pino-std-serializers@6.2.2: + resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==} + + /pino@8.15.0: + resolution: {integrity: sha512-olUADJByk4twxccmAxb1RiGKOSvddHugCV3wkqjyv+3Sooa2KLrmXrKEWOKi0XPCLasRR5jBXxioE1jxUa4KzQ==} + hasBin: true + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.3.0 + on-exit-leak-free: 2.1.0 + pino-abstract-transport: 1.0.0 + pino-std-serializers: 6.2.2 + process-warning: 2.2.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.4.3 + sonic-boom: 3.3.0 + thread-stream: 2.4.0 + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: true + + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + dev: true + + /postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + /postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + /postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + /postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + dependencies: + xtend: 4.0.2 + + /prebuild-install@7.1.1: + resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + detect-libc: 2.0.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.47.0 + pump: 3.0.0 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: false + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier-config-standard@5.0.0(prettier@2.7.1): + resolution: {integrity: sha512-QK252QwCxlsak8Zx+rPKZU31UdbRcu9iUk9X1ONYtLSO221OgvV9TlKoTf6iPDZtvF3vE2mkgzFIEgSUcGELSQ==} + peerDependencies: + prettier: ^2.4.0 + dependencies: + prettier: 2.7.1 + dev: true + + /prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + dependencies: + fast-diff: 1.3.0 + dev: true + + /prettier@2.7.1: + resolution: {integrity: sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + + /pretty-format@28.1.3: + resolution: {integrity: sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/schemas': 28.1.3 + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: true + + /process-warning@2.2.0: + resolution: {integrity: sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==} + + /process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + /promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + dev: true + + /promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + dev: true + + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + dev: true + + /protobufjs@7.2.5: + resolution: {integrity: sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==} + engines: {node: '>=12.0.0'} + requiresBuild: true + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 18.17.8 + long: 5.2.3 + dev: false + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + /punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + dev: false + + /punycode@2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + /quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + /rate-limiter-flexible@2.4.1: + resolution: {integrity: sha512-dgH4T44TzKVO9CLArNto62hJOwlWJMLUjVVr/ii0uUzZXEXthDNr7/yefW5z/1vvHAfycc1tnuiYyNJ8CTRB3g==} + dev: false + + /raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: false + + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: true + + /read-pkg@3.0.0: + resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} + engines: {node: '>=4'} + dependencies: + load-json-file: 4.0.0 + normalize-package-data: 2.5.0 + path-type: 3.0.0 + dev: true + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + /readable-stream@4.4.2: + resolution: {integrity: sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + /readable-web-to-node-stream@3.0.2: + resolution: {integrity: sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==} + engines: {node: '>=8'} + dependencies: + readable-stream: 3.6.2 + dev: false + + /real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + /redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + dev: false + + /redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + dependencies: + redis-errors: 1.2.0 + dev: false + + /regenerate-unicode-properties@10.1.0: + resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} + engines: {node: '>=4'} + dependencies: + regenerate: 1.4.2 + dev: true + + /regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + dev: true + + /regenerator-runtime@0.14.0: + resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + dev: true + + /regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + dependencies: + '@babel/runtime': 7.22.10 + dev: true + + /regexp.prototype.flags@1.5.0: + resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + functions-have-names: 1.2.3 + dev: true + + /regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + dev: true + + /regexpu-core@5.3.2: + resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} + engines: {node: '>=4'} + dependencies: + '@babel/regjsgen': 0.8.0 + regenerate: 1.4.2 + regenerate-unicode-properties: 10.1.0 + regjsparser: 0.9.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.1.0 + dev: true + + /regjsparser@0.9.1: + resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} + hasBin: true + dependencies: + jsesc: 0.5.0 + dev: true + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + /resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + dev: true + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: true + + /resolve.exports@1.1.1: + resolution: {integrity: sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==} + engines: {node: '>=10'} + dev: true + + /resolve@1.22.4: + resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /retry@0.10.1: + resolution: {integrity: sha512-ZXUSQYTHdl3uS7IuCehYfMzKyIDBNoAuUblvy5oGO5UJSUTmStUUVPXbA9Qxd173Bgre53yCQczQuHgRWAdvJQ==} + dev: false + + /retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + /rfdc@1.3.0: + resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /roarr@7.15.1: + resolution: {integrity: sha512-0ExL9rjOXeQPvQvQo8IcV8SR2GTXmDr1FQFlY2HiAV+gdVQjaVZNOx9d4FI2RqFFsd0sNsiw2TRS/8RU9g0ZfA==} + engines: {node: '>=12.0'} + dependencies: + boolean: 3.2.0 + fast-json-stringify: 5.8.0 + fast-printf: 1.6.9 + globalthis: 1.0.3 + safe-stable-stringify: 2.4.3 + semver-compare: 1.0.0 + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + + /rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + requiresBuild: true + dependencies: + tslib: 2.6.2 + dev: false + optional: true + + /safe-array-concat@1.0.0: + resolution: {integrity: sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-regex: 1.1.4 + dev: true + + /safe-stable-stringify@2.4.3: + resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} + engines: {node: '>=10'} + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + requiresBuild: true + + /secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + dev: true + + /semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + + /semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + /serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + + /set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + dev: true + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + /sharp@0.31.2: + resolution: {integrity: sha512-DUdNVEXgS5A97cTagSLIIp8dUZ/lZtk78iNVZgHdHbx1qnQR7JAHY0BnXnwwH39Iw+VKhO08CTYhIg0p98vQ5Q==} + engines: {node: '>=14.15.0'} + requiresBuild: true + dependencies: + color: 4.2.3 + detect-libc: 2.0.2 + node-addon-api: 5.1.0 + prebuild-install: 7.1.1 + semver: 7.5.4 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: false + + /shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + dependencies: + shebang-regex: 1.0.0 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /shell-quote@1.8.1: + resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + dev: true + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: true + + /simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: false + + /simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + dev: false + + /simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + dependencies: + is-arrayish: 0.3.2 + dev: false + + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: true + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + dev: true + + /socks-proxy-agent@7.0.0: + resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==} + engines: {node: '>= 10'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + socks: 2.7.1 + transitivePeerDependencies: + - supports-color + dev: true + + /socks@2.7.1: + resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} + engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} + dependencies: + ip: 2.0.0 + smart-buffer: 4.2.0 + dev: true + + /sonic-boom@3.3.0: + resolution: {integrity: sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==} + dependencies: + atomic-sleep: 1.0.0 + + /source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + /source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + dev: false + + /spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.13 + dev: true + + /spdx-exceptions@2.3.0: + resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} + dev: true + + /spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + dependencies: + spdx-exceptions: 2.3.0 + spdx-license-ids: 3.0.13 + dev: true + + /spdx-license-ids@3.0.13: + resolution: {integrity: sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==} + dev: true + + /split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + /split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + dependencies: + through: 2.3.8 + dev: false + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + + /ssri@9.0.1: + resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + minipass: 3.3.6 + dev: true + + /stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + dev: true + + /standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + dev: false + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + /stream-browserify@3.0.0: + resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: false + + /string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string.prototype.padend@3.1.4: + resolution: {integrity: sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: true + + /string.prototype.trim@1.2.7: + resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: true + + /string.prototype.trimend@1.0.6: + resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: true + + /string.prototype.trimstart@1.0.6: + resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + dev: true + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + dev: true + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: true + + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: false + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + dev: false + + /strtok3@6.3.0: + resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} + engines: {node: '>=10'} + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 4.1.0 + dev: false + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-hyperlinks@2.3.0: + resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + dev: false + + /tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: false + + /tar@6.1.15: + resolution: {integrity: sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==} + engines: {node: '>=10'} + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + dev: true + + /terminal-link@2.1.1: + resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} + engines: {node: '>=8'} + dependencies: + ansi-escapes: 4.3.2 + supports-hyperlinks: 2.3.0 + dev: true + + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /thread-stream@2.4.0: + resolution: {integrity: sha512-xZYtOtmnA63zj04Q+F9bdEay5r47bvpo1CaNqsKi7TpoJHcotUez8Fkfo2RJWpW91lnnaApdpRbVwCWsy+ifcw==} + dependencies: + real-require: 0.2.0 + + /through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + dev: false + + /tlds@1.234.0: + resolution: {integrity: sha512-TNDfeyDIC+oroH44bMbWC+Jn/2qNrfRvDK2EXt1icOXYG5NMqoRyUosADrukfb4D8lJ3S1waaBWSvQro0erdng==} + hasBin: true + dev: false + + /tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + /token-types@4.2.1: + resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} + engines: {node: '>=10'} + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + dev: false + + /ts-morph@16.0.0: + resolution: {integrity: sha512-jGNF0GVpFj0orFw55LTsQxVYEUOCWBAbR5Ls7fTYE5pQsbW18ssTb/6UXx/GYAEjS+DQTp8VoTw0vqYMiaaQuw==} + dependencies: + '@ts-morph/common': 0.17.0 + code-block-writer: 11.0.3 + dev: false + + /ts-node@10.8.2(@swc/core@1.3.42)(@types/node@18.0.0)(typescript@4.8.4): + resolution: {integrity: sha512-LYdGnoGddf1D6v8REPtIH+5iq/gTDuZqv2/UJUU7tKjuEU8xVZorBM+buCGNjj+pGEud+sOoM4CX3/YzINpENA==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@swc/core': 1.3.42 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.0.0 + 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: 4.8.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + + /ts-node@10.8.2(@swc/core@1.3.42)(@types/node@18.17.8)(typescript@4.9.5): + resolution: {integrity: sha512-LYdGnoGddf1D6v8REPtIH+5iq/gTDuZqv2/UJUU7tKjuEU8xVZorBM+buCGNjj+pGEud+sOoM4CX3/YzINpENA==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@swc/core': 1.3.42 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.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: 4.9.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + /tslib@2.3.1: + resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} + dev: true + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: false + + /tsutils@3.21.0(typescript@4.8.4): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 4.8.4 + dev: true + + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + dev: true + + /type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: true + + /typed-emitter@2.1.0: + resolution: {integrity: sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==} + optionalDependencies: + rxjs: 7.8.1 + dev: false + + /typescript@4.8.4: + resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /uglify-js@3.17.4: + resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + engines: {node: '>=0.8.0'} + hasBin: true + requiresBuild: true + dev: false + optional: true + + /uint8arrays@3.0.0: + resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==} + dependencies: + multiformats: 9.9.0 + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /unicode-canonical-property-names-ecmascript@2.0.0: + resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} + engines: {node: '>=4'} + dev: true + + /unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.0 + unicode-property-aliases-ecmascript: 2.1.0 + dev: true + + /unicode-match-property-value-ecmascript@2.1.0: + resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} + engines: {node: '>=4'} + dev: true + + /unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + dev: true + + /unique-filename@2.0.1: + resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + unique-slug: 3.0.0 + dev: true + + /unique-slug@3.0.0: + resolution: {integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + imurmurhash: 0.1.4 + dev: true + + /universalify@2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + dev: true + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + /update-browserslist-db@1.0.11(browserslist@4.21.10): + resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.21.10 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.0 + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: false + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + + /v8-to-istanbul@9.1.0: + resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + '@types/istanbul-lib-coverage': 2.0.4 + convert-source-map: 1.9.0 + dev: true + + /validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + dev: true + + /varint@6.0.0: + resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} + dev: false + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + /walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + dependencies: + makeerror: 1.0.12 + dev: true + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which-typed-array@1.1.11: + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + + /which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + dependencies: + string-width: 4.2.3 + dev: true + + /wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + dev: false + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + /write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + dev: true + + /ws@8.12.0: + resolution: {integrity: sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: true + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true + + /yesno@0.4.0: + resolution: {integrity: sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA==} + dev: false + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + /zod@3.21.4: + resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000000..985ef666321 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "services/*" + - "packages/*" diff --git a/packages/bsky/Dockerfile b/services/bsky/Dockerfile similarity index 70% rename from packages/bsky/Dockerfile rename to services/bsky/Dockerfile index 168e1316214..9da764ecc3d 100644 --- a/packages/bsky/Dockerfile +++ b/services/bsky/Dockerfile @@ -1,5 +1,7 @@ FROM node:18-alpine as build +RUN npm install -g pnpm + # Move files into the image and install WORKDIR /app COPY ./*.* ./ @@ -16,24 +18,30 @@ COPY ./packages/lexicon ./packages/lexicon COPY ./packages/repo ./packages/repo COPY ./packages/xrpc ./packages/xrpc COPY ./packages/xrpc-server ./packages/xrpc-server -RUN ATP_BUILD_SHALLOW=true yarn install --frozen-lockfile > /dev/null -RUN yarn workspaces run build --update-main-to-dist > /dev/null -# Remove non-prod deps -RUN yarn install --production --ignore-scripts --prefer-offline > /dev/null +COPY ./services/bsky ./services/bsky + +# 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 packages/bsky/service -RUN yarn install --frozen-lockfile > /dev/null +WORKDIR services/bsky # Uses assets from build stage to reduce build size FROM node:18-alpine -# RUN npm install -g yarn RUN apk add --update dumb-init # Avoid zombie processes, handle signal forwarding ENTRYPOINT ["dumb-init", "--"] -WORKDIR /app/packages/bsky/service +WORKDIR /app/services/bsky COPY --from=build /app /app EXPOSE 3000 diff --git a/packages/bsky/service/api.js b/services/bsky/api.js similarity index 100% rename from packages/bsky/service/api.js rename to services/bsky/api.js diff --git a/packages/bsky/service/indexer.js b/services/bsky/indexer.js similarity index 100% rename from packages/bsky/service/indexer.js rename to services/bsky/indexer.js diff --git a/packages/bsky/service/ingester.js b/services/bsky/ingester.js similarity index 100% rename from packages/bsky/service/ingester.js rename to services/bsky/ingester.js diff --git a/services/bsky/package.json b/services/bsky/package.json new file mode 100644 index 00000000000..65de10674dc --- /dev/null +++ b/services/bsky/package.json @@ -0,0 +1,9 @@ +{ + "name": "bsky-app-view-service", + "private": true, + "dependencies": { + "@atproto/aws": "workspace:^", + "@atproto/bsky": "workspace:^", + "dd-trace": "3.13.2" + } +} diff --git a/packages/pds/Dockerfile b/services/pds/Dockerfile similarity index 72% rename from packages/pds/Dockerfile rename to services/pds/Dockerfile index 01d4b74654c..c6d7d4351d0 100644 --- a/packages/pds/Dockerfile +++ b/services/pds/Dockerfile @@ -1,5 +1,7 @@ FROM node:18-alpine as build +RUN npm install -g pnpm + # Move files into the image and install WORKDIR /app COPY ./*.* ./ @@ -14,28 +16,33 @@ COPY ./packages/syntax ./packages/syntax COPY ./packages/identity ./packages/identity COPY ./packages/lex-cli ./packages/lex-cli COPY ./packages/lexicon ./packages/lexicon -COPY ./packages/pds ./packages/pds COPY ./packages/repo ./packages/repo COPY ./packages/xrpc ./packages/xrpc COPY ./packages/xrpc-server ./packages/xrpc-server -RUN ATP_BUILD_SHALLOW=true yarn install --frozen-lockfile > /dev/null -RUN yarn workspaces run build --update-main-to-dist > /dev/null -# Remove non-prod deps -RUN yarn install --production --ignore-scripts --prefer-offline > /dev/null +COPY ./services/pds ./services/pds + +# 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 packages/pds/service -RUN yarn install --frozen-lockfile > /dev/null +WORKDIR services/pds # Uses assets from build stage to reduce build size FROM node:18-alpine -# RUN npm install -g yarn RUN apk add --update dumb-init # Avoid zombie processes, handle signal forwarding ENTRYPOINT ["dumb-init", "--"] -WORKDIR /app/packages/pds/service +WORKDIR /app/services/pds COPY --from=build /app /app EXPOSE 3000 diff --git a/packages/pds/service/index.js b/services/pds/index.js similarity index 100% rename from packages/pds/service/index.js rename to services/pds/index.js diff --git a/services/pds/package.json b/services/pds/package.json new file mode 100644 index 00000000000..96729d806e6 --- /dev/null +++ b/services/pds/package.json @@ -0,0 +1,10 @@ +{ + "name": "plc-service", + "private": true, + "dependencies": { + "@atproto/aws": "workspace:^", + "@atproto/crypto": "workspace:^", + "@atproto/pds": "workspace:^", + "dd-trace": "3.13.2" + } +} diff --git a/update-main-to-dist.js b/update-main-to-dist.js new file mode 100644 index 00000000000..9ae0924bddf --- /dev/null +++ b/update-main-to-dist.js @@ -0,0 +1,11 @@ +const path = require('path') +const pkgJson = require('@npmcli/package-json') + +const [dir] = process.argv.slice(2) + +pkgJson + .load(path.resolve(__dirname, dir)) + .then((pkg) => + pkg.update({ main: pkg.content.publishConfig.main }) + ) + .then((pkg) => pkg.save()) diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 46f614845e7..00000000000 --- a/yarn.lock +++ /dev/null @@ -1,12205 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@ampproject/remapping@^2.1.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz" - integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== - dependencies: - "@jridgewell/gen-mapping" "^0.1.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@atproto/common@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.0.tgz#4216a8fef5b985ab62ac21252a0f8ca0f4a0f210" - integrity sha512-OB5tWE2R19jwiMIs2IjQieH5KTUuMb98XGCn9h3xuu6NanwjlmbCYMv08fMYwIp3UQ6jcq//84cDT3Bu6fJD+A== - dependencies: - "@ipld/dag-cbor" "^7.0.3" - multiformats "^9.6.4" - pino "^8.6.1" - zod "^3.14.2" - -"@atproto/crypto@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.1.0.tgz#bc73a479f9dbe06fa025301c182d7f7ab01bc568" - integrity sha512-9xgFEPtsCiJEPt9o3HtJT30IdFTGw5cQRSJVIy5CFhqBA4vDLcdXiRDLCjkzHEVbtNCsHUW6CrlfOgbeLPcmcg== - dependencies: - "@noble/secp256k1" "^1.7.0" - big-integer "^1.6.51" - multiformats "^9.6.4" - one-webcrypto "^1.0.3" - uint8arrays "3.0.0" - -"@aws-crypto/crc32@2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-2.0.0.tgz" - integrity sha512-TvE1r2CUueyXOuHdEigYjIZVesInd9KN+K/TFFNfkkxRThiNxO6i4ZqqAVMoEjAamZZ1AA8WXJkjCz7YShHPQA== - dependencies: - "@aws-crypto/util" "^2.0.0" - "@aws-sdk/types" "^3.1.0" - tslib "^1.11.1" - -"@aws-crypto/crc32c@2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-2.0.0.tgz" - integrity sha512-vF0eMdMHx3O3MoOXUfBZry8Y4ZDtcuskjjKgJz8YfIDjLStxTZrYXk+kZqtl6A0uCmmiN/Eb/JbC/CndTV1MHg== - dependencies: - "@aws-crypto/util" "^2.0.0" - "@aws-sdk/types" "^3.1.0" - tslib "^1.11.1" - -"@aws-crypto/ie11-detection@^2.0.0": - version "2.0.2" - resolved "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz" - integrity sha512-5XDMQY98gMAf/WRTic5G++jfmS/VLM0rwpiOpaainKi4L0nqWMSB1SzsrEG5rjFZGYN6ZAefO+/Yta2dFM0kMw== - dependencies: - tslib "^1.11.1" - -"@aws-crypto/ie11-detection@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz#640ae66b4ec3395cee6a8e94ebcd9f80c24cd688" - integrity sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q== - dependencies: - tslib "^1.11.1" - -"@aws-crypto/sha1-browser@2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-2.0.0.tgz" - integrity sha512-3fIVRjPFY8EG5HWXR+ZJZMdWNRpwbxGzJ9IH9q93FpbgCH8u8GHRi46mZXp3cYD7gealmyqpm3ThZwLKJjWJhA== - dependencies: - "@aws-crypto/ie11-detection" "^2.0.0" - "@aws-crypto/supports-web-crypto" "^2.0.0" - "@aws-sdk/types" "^3.1.0" - "@aws-sdk/util-locate-window" "^3.0.0" - "@aws-sdk/util-utf8-browser" "^3.0.0" - tslib "^1.11.1" - -"@aws-crypto/sha256-browser@2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-2.0.0.tgz" - integrity sha512-rYXOQ8BFOaqMEHJrLHul/25ckWH6GTJtdLSajhlqGMx0PmSueAuvboCuZCTqEKlxR8CQOwRarxYMZZSYlhRA1A== - dependencies: - "@aws-crypto/ie11-detection" "^2.0.0" - "@aws-crypto/sha256-js" "^2.0.0" - "@aws-crypto/supports-web-crypto" "^2.0.0" - "@aws-crypto/util" "^2.0.0" - "@aws-sdk/types" "^3.1.0" - "@aws-sdk/util-locate-window" "^3.0.0" - "@aws-sdk/util-utf8-browser" "^3.0.0" - tslib "^1.11.1" - -"@aws-crypto/sha256-browser@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz#05f160138ab893f1c6ba5be57cfd108f05827766" - integrity sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ== - dependencies: - "@aws-crypto/ie11-detection" "^3.0.0" - "@aws-crypto/sha256-js" "^3.0.0" - "@aws-crypto/supports-web-crypto" "^3.0.0" - "@aws-crypto/util" "^3.0.0" - "@aws-sdk/types" "^3.222.0" - "@aws-sdk/util-locate-window" "^3.0.0" - "@aws-sdk/util-utf8-browser" "^3.0.0" - tslib "^1.11.1" - -"@aws-crypto/sha256-js@2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-2.0.0.tgz" - integrity sha512-VZY+mCY4Nmrs5WGfitmNqXzaE873fcIZDu54cbaDaaamsaTOP1DBImV9F4pICc3EHjQXujyE8jig+PFCaew9ig== - dependencies: - "@aws-crypto/util" "^2.0.0" - "@aws-sdk/types" "^3.1.0" - tslib "^1.11.1" - -"@aws-crypto/sha256-js@3.0.0", "@aws-crypto/sha256-js@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz#f06b84d550d25521e60d2a0e2a90139341e007c2" - integrity sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ== - dependencies: - "@aws-crypto/util" "^3.0.0" - "@aws-sdk/types" "^3.222.0" - tslib "^1.11.1" - -"@aws-crypto/sha256-js@^2.0.0": - version "2.0.2" - resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-2.0.2.tgz" - integrity sha512-iXLdKH19qPmIC73fVCrHWCSYjN/sxaAvZ3jNNyw6FclmHyjLKg0f69WlC9KTnyElxCR5MO9SKaG00VwlJwyAkQ== - dependencies: - "@aws-crypto/util" "^2.0.2" - "@aws-sdk/types" "^3.110.0" - tslib "^1.11.1" - -"@aws-crypto/supports-web-crypto@^2.0.0": - version "2.0.2" - resolved "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-2.0.2.tgz" - integrity sha512-6mbSsLHwZ99CTOOswvCRP3C+VCWnzBf+1SnbWxzzJ9lR0mA0JnY2JEAhp8rqmTE0GPFy88rrM27ffgp62oErMQ== - dependencies: - tslib "^1.11.1" - -"@aws-crypto/supports-web-crypto@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz#5d1bf825afa8072af2717c3e455f35cda0103ec2" - integrity sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg== - dependencies: - tslib "^1.11.1" - -"@aws-crypto/util@^2.0.0", "@aws-crypto/util@^2.0.2": - version "2.0.2" - resolved "https://registry.npmjs.org/@aws-crypto/util/-/util-2.0.2.tgz" - integrity sha512-Lgu5v/0e/BcrZ5m/IWqzPUf3UYFTy/PpeED+uc9SWUR1iZQL8XXbGQg10UfllwwBryO3hFF5dizK+78aoXC1eA== - dependencies: - "@aws-sdk/types" "^3.110.0" - "@aws-sdk/util-utf8-browser" "^3.0.0" - tslib "^1.11.1" - -"@aws-crypto/util@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-3.0.0.tgz#1c7ca90c29293f0883468ad48117937f0fe5bfb0" - integrity sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w== - dependencies: - "@aws-sdk/types" "^3.222.0" - "@aws-sdk/util-utf8-browser" "^3.0.0" - tslib "^1.11.1" - -"@aws-sdk/abort-controller@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.193.0.tgz" - integrity sha512-MYPBm5PWyKP+Tq37mKs5wDbyAyVMocF5iYmx738LYXBSj8A1V4LTFrvfd4U16BRC/sM0DYB9fBFJUQ9ISFRVYw== - dependencies: - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/abort-controller@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.224.0.tgz" - integrity sha512-6DxaHnSDc2V5WiwtDaRwJJb2fkmDTyGr1svIM9H671aXIwe+q17mtpm5IooKL8bW5mLJoB1pT/5ntLkfxDQgSQ== - dependencies: - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/abort-controller@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/abort-controller/-/abort-controller-3.257.0.tgz#a9039bd9c409defbbeb7bafef3a1b206fbfedad1" - integrity sha512-ekWy391lOerS0ZECdhp/c+X7AToJIpfNrCPjuj3bKr+GMQYckGsYsdbm6AUD4sxBmfvuaQmVniSXWovaxwcFcQ== - dependencies: - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/chunked-blob-reader-native@3.208.0": - version "3.208.0" - resolved "https://registry.npmjs.org/@aws-sdk/chunked-blob-reader-native/-/chunked-blob-reader-native-3.208.0.tgz" - integrity sha512-JeOZ95PW+fJ6bbuqPySYqLqHk1n4+4ueEEraJsiUrPBV0S1ZtyvOGHcnGztKUjr2PYNaiexmpWuvUve9K12HRA== - dependencies: - "@aws-sdk/util-base64" "3.208.0" - tslib "^2.3.1" - -"@aws-sdk/chunked-blob-reader@3.188.0": - version "3.188.0" - resolved "https://registry.npmjs.org/@aws-sdk/chunked-blob-reader/-/chunked-blob-reader-3.188.0.tgz" - integrity sha512-zkPRFZZPL3eH+kH86LDYYXImiClA1/sW60zYOjse9Pgka+eDJlvBN6hcYxwDEKjcwATYiSRR1aVQHcfCinlGXg== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/client-cloudfront@^3.261.0": - version "3.261.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-cloudfront/-/client-cloudfront-3.261.0.tgz#83503e3f561d8795f5a0f3415c4b379d68fed082" - integrity sha512-7JOpLfgYdQ+CDA3McsAmzcCO+rZj3wVicNTF7Kpl0JaZ0NB0NShifMb4OAGuh2RNh+OYV6k3mtjsXh9ZIQ08PQ== - dependencies: - "@aws-crypto/sha256-browser" "3.0.0" - "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/client-sts" "3.261.0" - "@aws-sdk/config-resolver" "3.259.0" - "@aws-sdk/credential-provider-node" "3.261.0" - "@aws-sdk/fetch-http-handler" "3.257.0" - "@aws-sdk/hash-node" "3.257.0" - "@aws-sdk/invalid-dependency" "3.257.0" - "@aws-sdk/middleware-content-length" "3.257.0" - "@aws-sdk/middleware-endpoint" "3.257.0" - "@aws-sdk/middleware-host-header" "3.257.0" - "@aws-sdk/middleware-logger" "3.257.0" - "@aws-sdk/middleware-recursion-detection" "3.257.0" - "@aws-sdk/middleware-retry" "3.259.0" - "@aws-sdk/middleware-serde" "3.257.0" - "@aws-sdk/middleware-signing" "3.257.0" - "@aws-sdk/middleware-stack" "3.257.0" - "@aws-sdk/middleware-user-agent" "3.257.0" - "@aws-sdk/node-config-provider" "3.259.0" - "@aws-sdk/node-http-handler" "3.257.0" - "@aws-sdk/protocol-http" "3.257.0" - "@aws-sdk/smithy-client" "3.261.0" - "@aws-sdk/types" "3.257.0" - "@aws-sdk/url-parser" "3.257.0" - "@aws-sdk/util-base64" "3.208.0" - "@aws-sdk/util-body-length-browser" "3.188.0" - "@aws-sdk/util-body-length-node" "3.208.0" - "@aws-sdk/util-defaults-mode-browser" "3.261.0" - "@aws-sdk/util-defaults-mode-node" "3.261.0" - "@aws-sdk/util-endpoints" "3.257.0" - "@aws-sdk/util-retry" "3.257.0" - "@aws-sdk/util-user-agent-browser" "3.257.0" - "@aws-sdk/util-user-agent-node" "3.259.0" - "@aws-sdk/util-utf8" "3.254.0" - "@aws-sdk/util-waiter" "3.257.0" - "@aws-sdk/xml-builder" "3.201.0" - fast-xml-parser "4.0.11" - tslib "^2.3.1" - -"@aws-sdk/client-kms@^3.196.0": - version "3.196.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-kms/-/client-kms-3.196.0.tgz" - integrity sha512-mR5jxfvHnv71FLd87PJ0KNgVXcZzNvKiI3i3JyLmukapnN5Kz2n0cG/jruo9d29zYQS60kfIPjdHddzOxNHH4A== - dependencies: - "@aws-crypto/sha256-browser" "2.0.0" - "@aws-crypto/sha256-js" "2.0.0" - "@aws-sdk/client-sts" "3.196.0" - "@aws-sdk/config-resolver" "3.193.0" - "@aws-sdk/credential-provider-node" "3.196.0" - "@aws-sdk/fetch-http-handler" "3.193.0" - "@aws-sdk/hash-node" "3.193.0" - "@aws-sdk/invalid-dependency" "3.193.0" - "@aws-sdk/middleware-content-length" "3.193.0" - "@aws-sdk/middleware-endpoint" "3.193.0" - "@aws-sdk/middleware-host-header" "3.193.0" - "@aws-sdk/middleware-logger" "3.193.0" - "@aws-sdk/middleware-recursion-detection" "3.193.0" - "@aws-sdk/middleware-retry" "3.193.0" - "@aws-sdk/middleware-serde" "3.193.0" - "@aws-sdk/middleware-signing" "3.193.0" - "@aws-sdk/middleware-stack" "3.193.0" - "@aws-sdk/middleware-user-agent" "3.193.0" - "@aws-sdk/node-config-provider" "3.193.0" - "@aws-sdk/node-http-handler" "3.193.0" - "@aws-sdk/protocol-http" "3.193.0" - "@aws-sdk/smithy-client" "3.193.0" - "@aws-sdk/types" "3.193.0" - "@aws-sdk/url-parser" "3.193.0" - "@aws-sdk/util-base64-browser" "3.188.0" - "@aws-sdk/util-base64-node" "3.188.0" - "@aws-sdk/util-body-length-browser" "3.188.0" - "@aws-sdk/util-body-length-node" "3.188.0" - "@aws-sdk/util-defaults-mode-browser" "3.193.0" - "@aws-sdk/util-defaults-mode-node" "3.193.0" - "@aws-sdk/util-endpoints" "3.196.0" - "@aws-sdk/util-user-agent-browser" "3.193.0" - "@aws-sdk/util-user-agent-node" "3.193.0" - "@aws-sdk/util-utf8-browser" "3.188.0" - "@aws-sdk/util-utf8-node" "3.188.0" - tslib "^2.3.1" - -"@aws-sdk/client-s3@^3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.224.0.tgz" - integrity sha512-CPU1sG4xr+fJ+OFpqz9Oum7cJwas0mA9YFvPLkgKLvNC2rhmmn0kbjwawtc6GUDu6xygeV8koBL2gz7OJHQ7fQ== - dependencies: - "@aws-crypto/sha1-browser" "2.0.0" - "@aws-crypto/sha256-browser" "2.0.0" - "@aws-crypto/sha256-js" "2.0.0" - "@aws-sdk/client-sts" "3.224.0" - "@aws-sdk/config-resolver" "3.224.0" - "@aws-sdk/credential-provider-node" "3.224.0" - "@aws-sdk/eventstream-serde-browser" "3.224.0" - "@aws-sdk/eventstream-serde-config-resolver" "3.224.0" - "@aws-sdk/eventstream-serde-node" "3.224.0" - "@aws-sdk/fetch-http-handler" "3.224.0" - "@aws-sdk/hash-blob-browser" "3.224.0" - "@aws-sdk/hash-node" "3.224.0" - "@aws-sdk/hash-stream-node" "3.224.0" - "@aws-sdk/invalid-dependency" "3.224.0" - "@aws-sdk/md5-js" "3.224.0" - "@aws-sdk/middleware-bucket-endpoint" "3.224.0" - "@aws-sdk/middleware-content-length" "3.224.0" - "@aws-sdk/middleware-endpoint" "3.224.0" - "@aws-sdk/middleware-expect-continue" "3.224.0" - "@aws-sdk/middleware-flexible-checksums" "3.224.0" - "@aws-sdk/middleware-host-header" "3.224.0" - "@aws-sdk/middleware-location-constraint" "3.224.0" - "@aws-sdk/middleware-logger" "3.224.0" - "@aws-sdk/middleware-recursion-detection" "3.224.0" - "@aws-sdk/middleware-retry" "3.224.0" - "@aws-sdk/middleware-sdk-s3" "3.224.0" - "@aws-sdk/middleware-serde" "3.224.0" - "@aws-sdk/middleware-signing" "3.224.0" - "@aws-sdk/middleware-ssec" "3.224.0" - "@aws-sdk/middleware-stack" "3.224.0" - "@aws-sdk/middleware-user-agent" "3.224.0" - "@aws-sdk/node-config-provider" "3.224.0" - "@aws-sdk/node-http-handler" "3.224.0" - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/signature-v4-multi-region" "3.224.0" - "@aws-sdk/smithy-client" "3.224.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/url-parser" "3.224.0" - "@aws-sdk/util-base64" "3.208.0" - "@aws-sdk/util-body-length-browser" "3.188.0" - "@aws-sdk/util-body-length-node" "3.208.0" - "@aws-sdk/util-defaults-mode-browser" "3.224.0" - "@aws-sdk/util-defaults-mode-node" "3.224.0" - "@aws-sdk/util-endpoints" "3.224.0" - "@aws-sdk/util-stream-browser" "3.224.0" - "@aws-sdk/util-stream-node" "3.224.0" - "@aws-sdk/util-user-agent-browser" "3.224.0" - "@aws-sdk/util-user-agent-node" "3.224.0" - "@aws-sdk/util-utf8-browser" "3.188.0" - "@aws-sdk/util-utf8-node" "3.208.0" - "@aws-sdk/util-waiter" "3.224.0" - "@aws-sdk/xml-builder" "3.201.0" - fast-xml-parser "4.0.11" - tslib "^2.3.1" - -"@aws-sdk/client-sso-oidc@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.224.0.tgz" - integrity sha512-r7QAqinMvuZvGlfC4ltEBIq3gJ1AI4tTqEi8lG06+gDoiwnqTWii0+OrZJQiaeLc3PqDHwxmRpEmjFlr/f5TKg== - dependencies: - "@aws-crypto/sha256-browser" "2.0.0" - "@aws-crypto/sha256-js" "2.0.0" - "@aws-sdk/config-resolver" "3.224.0" - "@aws-sdk/fetch-http-handler" "3.224.0" - "@aws-sdk/hash-node" "3.224.0" - "@aws-sdk/invalid-dependency" "3.224.0" - "@aws-sdk/middleware-content-length" "3.224.0" - "@aws-sdk/middleware-endpoint" "3.224.0" - "@aws-sdk/middleware-host-header" "3.224.0" - "@aws-sdk/middleware-logger" "3.224.0" - "@aws-sdk/middleware-recursion-detection" "3.224.0" - "@aws-sdk/middleware-retry" "3.224.0" - "@aws-sdk/middleware-serde" "3.224.0" - "@aws-sdk/middleware-stack" "3.224.0" - "@aws-sdk/middleware-user-agent" "3.224.0" - "@aws-sdk/node-config-provider" "3.224.0" - "@aws-sdk/node-http-handler" "3.224.0" - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/smithy-client" "3.224.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/url-parser" "3.224.0" - "@aws-sdk/util-base64" "3.208.0" - "@aws-sdk/util-body-length-browser" "3.188.0" - "@aws-sdk/util-body-length-node" "3.208.0" - "@aws-sdk/util-defaults-mode-browser" "3.224.0" - "@aws-sdk/util-defaults-mode-node" "3.224.0" - "@aws-sdk/util-endpoints" "3.224.0" - "@aws-sdk/util-user-agent-browser" "3.224.0" - "@aws-sdk/util-user-agent-node" "3.224.0" - "@aws-sdk/util-utf8-browser" "3.188.0" - "@aws-sdk/util-utf8-node" "3.208.0" - tslib "^2.3.1" - -"@aws-sdk/client-sso-oidc@3.261.0": - version "3.261.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.261.0.tgz#437e5b4ccc37bcc14e94afead8eae909887e8309" - integrity sha512-ItgRT/BThv2UxEeGJ5/GCF6JY1Rzk39IcDIPZAfBA8HbYcznXGDsBTRf45MErS+uollwNFX0T/WNlTbmjEDE7g== - dependencies: - "@aws-crypto/sha256-browser" "3.0.0" - "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/config-resolver" "3.259.0" - "@aws-sdk/fetch-http-handler" "3.257.0" - "@aws-sdk/hash-node" "3.257.0" - "@aws-sdk/invalid-dependency" "3.257.0" - "@aws-sdk/middleware-content-length" "3.257.0" - "@aws-sdk/middleware-endpoint" "3.257.0" - "@aws-sdk/middleware-host-header" "3.257.0" - "@aws-sdk/middleware-logger" "3.257.0" - "@aws-sdk/middleware-recursion-detection" "3.257.0" - "@aws-sdk/middleware-retry" "3.259.0" - "@aws-sdk/middleware-serde" "3.257.0" - "@aws-sdk/middleware-stack" "3.257.0" - "@aws-sdk/middleware-user-agent" "3.257.0" - "@aws-sdk/node-config-provider" "3.259.0" - "@aws-sdk/node-http-handler" "3.257.0" - "@aws-sdk/protocol-http" "3.257.0" - "@aws-sdk/smithy-client" "3.261.0" - "@aws-sdk/types" "3.257.0" - "@aws-sdk/url-parser" "3.257.0" - "@aws-sdk/util-base64" "3.208.0" - "@aws-sdk/util-body-length-browser" "3.188.0" - "@aws-sdk/util-body-length-node" "3.208.0" - "@aws-sdk/util-defaults-mode-browser" "3.261.0" - "@aws-sdk/util-defaults-mode-node" "3.261.0" - "@aws-sdk/util-endpoints" "3.257.0" - "@aws-sdk/util-retry" "3.257.0" - "@aws-sdk/util-user-agent-browser" "3.257.0" - "@aws-sdk/util-user-agent-node" "3.259.0" - "@aws-sdk/util-utf8" "3.254.0" - tslib "^2.3.1" - -"@aws-sdk/client-sso@3.196.0": - version "3.196.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.196.0.tgz" - integrity sha512-u+UnxrVHLjLDdfCZft1AuyIhyv+77/inCHR4LcKsGASRA+jAg3z+OY+B7Q9hWHNcVt5ECMw7rxe4jA9BLf42sw== - dependencies: - "@aws-crypto/sha256-browser" "2.0.0" - "@aws-crypto/sha256-js" "2.0.0" - "@aws-sdk/config-resolver" "3.193.0" - "@aws-sdk/fetch-http-handler" "3.193.0" - "@aws-sdk/hash-node" "3.193.0" - "@aws-sdk/invalid-dependency" "3.193.0" - "@aws-sdk/middleware-content-length" "3.193.0" - "@aws-sdk/middleware-endpoint" "3.193.0" - "@aws-sdk/middleware-host-header" "3.193.0" - "@aws-sdk/middleware-logger" "3.193.0" - "@aws-sdk/middleware-recursion-detection" "3.193.0" - "@aws-sdk/middleware-retry" "3.193.0" - "@aws-sdk/middleware-serde" "3.193.0" - "@aws-sdk/middleware-stack" "3.193.0" - "@aws-sdk/middleware-user-agent" "3.193.0" - "@aws-sdk/node-config-provider" "3.193.0" - "@aws-sdk/node-http-handler" "3.193.0" - "@aws-sdk/protocol-http" "3.193.0" - "@aws-sdk/smithy-client" "3.193.0" - "@aws-sdk/types" "3.193.0" - "@aws-sdk/url-parser" "3.193.0" - "@aws-sdk/util-base64-browser" "3.188.0" - "@aws-sdk/util-base64-node" "3.188.0" - "@aws-sdk/util-body-length-browser" "3.188.0" - "@aws-sdk/util-body-length-node" "3.188.0" - "@aws-sdk/util-defaults-mode-browser" "3.193.0" - "@aws-sdk/util-defaults-mode-node" "3.193.0" - "@aws-sdk/util-endpoints" "3.196.0" - "@aws-sdk/util-user-agent-browser" "3.193.0" - "@aws-sdk/util-user-agent-node" "3.193.0" - "@aws-sdk/util-utf8-browser" "3.188.0" - "@aws-sdk/util-utf8-node" "3.188.0" - tslib "^2.3.1" - -"@aws-sdk/client-sso@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.224.0.tgz" - integrity sha512-ZfqjGGBhv+sKxYN9FHbepaL+ucFbAFndvNdalGj4mZsv5AqxgemkFoRofNJk4nu79JVf5cdrj7zL+BDW3KwEGg== - dependencies: - "@aws-crypto/sha256-browser" "2.0.0" - "@aws-crypto/sha256-js" "2.0.0" - "@aws-sdk/config-resolver" "3.224.0" - "@aws-sdk/fetch-http-handler" "3.224.0" - "@aws-sdk/hash-node" "3.224.0" - "@aws-sdk/invalid-dependency" "3.224.0" - "@aws-sdk/middleware-content-length" "3.224.0" - "@aws-sdk/middleware-endpoint" "3.224.0" - "@aws-sdk/middleware-host-header" "3.224.0" - "@aws-sdk/middleware-logger" "3.224.0" - "@aws-sdk/middleware-recursion-detection" "3.224.0" - "@aws-sdk/middleware-retry" "3.224.0" - "@aws-sdk/middleware-serde" "3.224.0" - "@aws-sdk/middleware-stack" "3.224.0" - "@aws-sdk/middleware-user-agent" "3.224.0" - "@aws-sdk/node-config-provider" "3.224.0" - "@aws-sdk/node-http-handler" "3.224.0" - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/smithy-client" "3.224.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/url-parser" "3.224.0" - "@aws-sdk/util-base64" "3.208.0" - "@aws-sdk/util-body-length-browser" "3.188.0" - "@aws-sdk/util-body-length-node" "3.208.0" - "@aws-sdk/util-defaults-mode-browser" "3.224.0" - "@aws-sdk/util-defaults-mode-node" "3.224.0" - "@aws-sdk/util-endpoints" "3.224.0" - "@aws-sdk/util-user-agent-browser" "3.224.0" - "@aws-sdk/util-user-agent-node" "3.224.0" - "@aws-sdk/util-utf8-browser" "3.188.0" - "@aws-sdk/util-utf8-node" "3.208.0" - tslib "^2.3.1" - -"@aws-sdk/client-sso@3.261.0": - version "3.261.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.261.0.tgz#9ab7dfed385d9a18e68dc16e7dedbd9619db4f8e" - integrity sha512-tq5hu1WXa9BKsCH9zOBOykyiaoZQvaFHKdOamw5SZ69niyO3AG4xR1TkLqXj/9mDYMLgAIVObKZDGWtBLFTdiQ== - dependencies: - "@aws-crypto/sha256-browser" "3.0.0" - "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/config-resolver" "3.259.0" - "@aws-sdk/fetch-http-handler" "3.257.0" - "@aws-sdk/hash-node" "3.257.0" - "@aws-sdk/invalid-dependency" "3.257.0" - "@aws-sdk/middleware-content-length" "3.257.0" - "@aws-sdk/middleware-endpoint" "3.257.0" - "@aws-sdk/middleware-host-header" "3.257.0" - "@aws-sdk/middleware-logger" "3.257.0" - "@aws-sdk/middleware-recursion-detection" "3.257.0" - "@aws-sdk/middleware-retry" "3.259.0" - "@aws-sdk/middleware-serde" "3.257.0" - "@aws-sdk/middleware-stack" "3.257.0" - "@aws-sdk/middleware-user-agent" "3.257.0" - "@aws-sdk/node-config-provider" "3.259.0" - "@aws-sdk/node-http-handler" "3.257.0" - "@aws-sdk/protocol-http" "3.257.0" - "@aws-sdk/smithy-client" "3.261.0" - "@aws-sdk/types" "3.257.0" - "@aws-sdk/url-parser" "3.257.0" - "@aws-sdk/util-base64" "3.208.0" - "@aws-sdk/util-body-length-browser" "3.188.0" - "@aws-sdk/util-body-length-node" "3.208.0" - "@aws-sdk/util-defaults-mode-browser" "3.261.0" - "@aws-sdk/util-defaults-mode-node" "3.261.0" - "@aws-sdk/util-endpoints" "3.257.0" - "@aws-sdk/util-retry" "3.257.0" - "@aws-sdk/util-user-agent-browser" "3.257.0" - "@aws-sdk/util-user-agent-node" "3.259.0" - "@aws-sdk/util-utf8" "3.254.0" - tslib "^2.3.1" - -"@aws-sdk/client-sts@3.196.0": - version "3.196.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.196.0.tgz" - integrity sha512-ChzK8606CugwnRLm7iwerXzeMqOsjGLe3j1j1HtQShzXZu4/ysQ3mUBBPAt2Lltx+1ep8MoI9vaQVyfw5h35ww== - dependencies: - "@aws-crypto/sha256-browser" "2.0.0" - "@aws-crypto/sha256-js" "2.0.0" - "@aws-sdk/config-resolver" "3.193.0" - "@aws-sdk/credential-provider-node" "3.196.0" - "@aws-sdk/fetch-http-handler" "3.193.0" - "@aws-sdk/hash-node" "3.193.0" - "@aws-sdk/invalid-dependency" "3.193.0" - "@aws-sdk/middleware-content-length" "3.193.0" - "@aws-sdk/middleware-endpoint" "3.193.0" - "@aws-sdk/middleware-host-header" "3.193.0" - "@aws-sdk/middleware-logger" "3.193.0" - "@aws-sdk/middleware-recursion-detection" "3.193.0" - "@aws-sdk/middleware-retry" "3.193.0" - "@aws-sdk/middleware-sdk-sts" "3.193.0" - "@aws-sdk/middleware-serde" "3.193.0" - "@aws-sdk/middleware-signing" "3.193.0" - "@aws-sdk/middleware-stack" "3.193.0" - "@aws-sdk/middleware-user-agent" "3.193.0" - "@aws-sdk/node-config-provider" "3.193.0" - "@aws-sdk/node-http-handler" "3.193.0" - "@aws-sdk/protocol-http" "3.193.0" - "@aws-sdk/smithy-client" "3.193.0" - "@aws-sdk/types" "3.193.0" - "@aws-sdk/url-parser" "3.193.0" - "@aws-sdk/util-base64-browser" "3.188.0" - "@aws-sdk/util-base64-node" "3.188.0" - "@aws-sdk/util-body-length-browser" "3.188.0" - "@aws-sdk/util-body-length-node" "3.188.0" - "@aws-sdk/util-defaults-mode-browser" "3.193.0" - "@aws-sdk/util-defaults-mode-node" "3.193.0" - "@aws-sdk/util-endpoints" "3.196.0" - "@aws-sdk/util-user-agent-browser" "3.193.0" - "@aws-sdk/util-user-agent-node" "3.193.0" - "@aws-sdk/util-utf8-browser" "3.188.0" - "@aws-sdk/util-utf8-node" "3.188.0" - fast-xml-parser "4.0.11" - tslib "^2.3.1" - -"@aws-sdk/client-sts@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.224.0.tgz" - integrity sha512-ao3jyjwk2fozk1d4PtrNf0BNsucPWAbALv8CCsPTC3r9g2Lg/TOi3pxmsfd69ddw89XSyP6zZATEHaWO+tk0CQ== - dependencies: - "@aws-crypto/sha256-browser" "2.0.0" - "@aws-crypto/sha256-js" "2.0.0" - "@aws-sdk/config-resolver" "3.224.0" - "@aws-sdk/credential-provider-node" "3.224.0" - "@aws-sdk/fetch-http-handler" "3.224.0" - "@aws-sdk/hash-node" "3.224.0" - "@aws-sdk/invalid-dependency" "3.224.0" - "@aws-sdk/middleware-content-length" "3.224.0" - "@aws-sdk/middleware-endpoint" "3.224.0" - "@aws-sdk/middleware-host-header" "3.224.0" - "@aws-sdk/middleware-logger" "3.224.0" - "@aws-sdk/middleware-recursion-detection" "3.224.0" - "@aws-sdk/middleware-retry" "3.224.0" - "@aws-sdk/middleware-sdk-sts" "3.224.0" - "@aws-sdk/middleware-serde" "3.224.0" - "@aws-sdk/middleware-signing" "3.224.0" - "@aws-sdk/middleware-stack" "3.224.0" - "@aws-sdk/middleware-user-agent" "3.224.0" - "@aws-sdk/node-config-provider" "3.224.0" - "@aws-sdk/node-http-handler" "3.224.0" - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/smithy-client" "3.224.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/url-parser" "3.224.0" - "@aws-sdk/util-base64" "3.208.0" - "@aws-sdk/util-body-length-browser" "3.188.0" - "@aws-sdk/util-body-length-node" "3.208.0" - "@aws-sdk/util-defaults-mode-browser" "3.224.0" - "@aws-sdk/util-defaults-mode-node" "3.224.0" - "@aws-sdk/util-endpoints" "3.224.0" - "@aws-sdk/util-user-agent-browser" "3.224.0" - "@aws-sdk/util-user-agent-node" "3.224.0" - "@aws-sdk/util-utf8-browser" "3.188.0" - "@aws-sdk/util-utf8-node" "3.208.0" - fast-xml-parser "4.0.11" - tslib "^2.3.1" - -"@aws-sdk/client-sts@3.261.0": - version "3.261.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.261.0.tgz#0ff6709b1b4a4db42584f9eef1ea58c19e38765f" - integrity sha512-jnCKBjuHEMgwCmR9bXDVpl/WzpUQyU9DL3Mk65XYyZwRxgHSaw5D90zRouoZMUneNA2OnKZQnjk6oyL47mb7oA== - dependencies: - "@aws-crypto/sha256-browser" "3.0.0" - "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/config-resolver" "3.259.0" - "@aws-sdk/credential-provider-node" "3.261.0" - "@aws-sdk/fetch-http-handler" "3.257.0" - "@aws-sdk/hash-node" "3.257.0" - "@aws-sdk/invalid-dependency" "3.257.0" - "@aws-sdk/middleware-content-length" "3.257.0" - "@aws-sdk/middleware-endpoint" "3.257.0" - "@aws-sdk/middleware-host-header" "3.257.0" - "@aws-sdk/middleware-logger" "3.257.0" - "@aws-sdk/middleware-recursion-detection" "3.257.0" - "@aws-sdk/middleware-retry" "3.259.0" - "@aws-sdk/middleware-sdk-sts" "3.257.0" - "@aws-sdk/middleware-serde" "3.257.0" - "@aws-sdk/middleware-signing" "3.257.0" - "@aws-sdk/middleware-stack" "3.257.0" - "@aws-sdk/middleware-user-agent" "3.257.0" - "@aws-sdk/node-config-provider" "3.259.0" - "@aws-sdk/node-http-handler" "3.257.0" - "@aws-sdk/protocol-http" "3.257.0" - "@aws-sdk/smithy-client" "3.261.0" - "@aws-sdk/types" "3.257.0" - "@aws-sdk/url-parser" "3.257.0" - "@aws-sdk/util-base64" "3.208.0" - "@aws-sdk/util-body-length-browser" "3.188.0" - "@aws-sdk/util-body-length-node" "3.208.0" - "@aws-sdk/util-defaults-mode-browser" "3.261.0" - "@aws-sdk/util-defaults-mode-node" "3.261.0" - "@aws-sdk/util-endpoints" "3.257.0" - "@aws-sdk/util-retry" "3.257.0" - "@aws-sdk/util-user-agent-browser" "3.257.0" - "@aws-sdk/util-user-agent-node" "3.259.0" - "@aws-sdk/util-utf8" "3.254.0" - fast-xml-parser "4.0.11" - tslib "^2.3.1" - -"@aws-sdk/config-resolver@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.193.0.tgz" - integrity sha512-HIjuv2A1glgkXy9g/A8bfsiz3jTFaRbwGZheoHFZod6iEQQEbbeAsBe3u2AZyzOrVLgs8lOvBtgU8XKSJWjDkw== - dependencies: - "@aws-sdk/signature-v4" "3.193.0" - "@aws-sdk/types" "3.193.0" - "@aws-sdk/util-config-provider" "3.188.0" - "@aws-sdk/util-middleware" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/config-resolver@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.224.0.tgz" - integrity sha512-jS53QvF2jdv7d6cpPUH6N85i1WNHik1eGvxqSndsNbLf0keEGXYyN4pBLNB0xK1nk0ZG+8slRsXgWvWTCcFYKA== - dependencies: - "@aws-sdk/signature-v4" "3.224.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/util-config-provider" "3.208.0" - "@aws-sdk/util-middleware" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/config-resolver@3.259.0": - version "3.259.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/config-resolver/-/config-resolver-3.259.0.tgz#b2c17b681f890dbe31bc1670da41ae653a734c84" - integrity sha512-gViMRsc4Ye6+nzJ0OYTZIT8m4glIAdtugN2Sr/t6P2iJW5X0bSL/EcbcHBgsve1lHjeGPeyzVkT7UnyGOZ5Z/A== - dependencies: - "@aws-sdk/signature-v4" "3.257.0" - "@aws-sdk/types" "3.257.0" - "@aws-sdk/util-config-provider" "3.208.0" - "@aws-sdk/util-middleware" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-env@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.193.0.tgz" - integrity sha512-pRqZoIaqCdWB4JJdR6DqDn3u+CwKJchwiCPnRtChwC8KXCMkT4njq9J1bWG3imYeTxP/G06O1PDONEuD4pPtNQ== - dependencies: - "@aws-sdk/property-provider" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-env@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.224.0.tgz" - integrity sha512-WUicVivCne9Ela2Nuufohy8+UV/W6GwanlpK9trJqrqHt2/zqdNYHqZbWL0zDNO8dvFN3+MC2a8boYPyR+cFRg== - dependencies: - "@aws-sdk/property-provider" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-env@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.257.0.tgz#131d06bafa738c7f2ce2e7ee12c227ff6a414ada" - integrity sha512-GsmBi5Di6hk1JAi1iB6/LCY8o+GmlCvJoB7wuoVmXI3VxRVwptUVjuj8EtJbIrVGrF9dSuIRPCzUoSuzEzYGlg== - dependencies: - "@aws-sdk/property-provider" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-imds@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.193.0.tgz" - integrity sha512-jC7uT7uVpO/iitz49toHMGFKXQ2igWQQG2SKirREqDRaz5HSXwEP1V3rcOlNNyGIBPMggDjZnxYgJHqBXSq9Ag== - dependencies: - "@aws-sdk/node-config-provider" "3.193.0" - "@aws-sdk/property-provider" "3.193.0" - "@aws-sdk/types" "3.193.0" - "@aws-sdk/url-parser" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-imds@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.224.0.tgz" - integrity sha512-n7uVR5Z9EUfVbg0gSNrJvu1g0cM/HqhRt+kaRJBGNf4q1tEbnCukKj+qUZbT1qdbDTyu9NTRphMvuIyN3RBDtQ== - dependencies: - "@aws-sdk/node-config-provider" "3.224.0" - "@aws-sdk/property-provider" "3.224.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/url-parser" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-imds@3.259.0": - version "3.259.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.259.0.tgz#23bfa858dd4e97a6d530b9e3b0f4497ab0a0f8c7" - integrity sha512-yCxoYWZAaDrCUEWxRfrpB0Mp1cFgJEMYW8T6GIb/+DQ5QLpZmorgaVD/j90QXupqFrR5tlxwuskBIkdD2E9YNg== - dependencies: - "@aws-sdk/node-config-provider" "3.259.0" - "@aws-sdk/property-provider" "3.257.0" - "@aws-sdk/types" "3.257.0" - "@aws-sdk/url-parser" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-ini@3.196.0": - version "3.196.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.196.0.tgz" - integrity sha512-3lL+YLBQ9KwQxG4AdRm4u2cvBNZeBmS/i3BWnCPomg96lNGPMrTEloVaVEpnrzOff6sgFxRtjkbLkVxmdipIrw== - dependencies: - "@aws-sdk/credential-provider-env" "3.193.0" - "@aws-sdk/credential-provider-imds" "3.193.0" - "@aws-sdk/credential-provider-sso" "3.196.0" - "@aws-sdk/credential-provider-web-identity" "3.193.0" - "@aws-sdk/property-provider" "3.193.0" - "@aws-sdk/shared-ini-file-loader" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-ini@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.224.0.tgz" - integrity sha512-YaAHoHJVspqy5f8C6EXBifMfodKXl88IHuL6eBComigTPR3s1Ed1+3AJdjA1X7SjAHfrYna/WvZEH3e8NCSzFA== - dependencies: - "@aws-sdk/credential-provider-env" "3.224.0" - "@aws-sdk/credential-provider-imds" "3.224.0" - "@aws-sdk/credential-provider-sso" "3.224.0" - "@aws-sdk/credential-provider-web-identity" "3.224.0" - "@aws-sdk/property-provider" "3.224.0" - "@aws-sdk/shared-ini-file-loader" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-ini@3.261.0": - version "3.261.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.261.0.tgz#435525bd8d8ceb28ee69a628e22c8f0ee5af1dca" - integrity sha512-638jTnvFbGO0G0So+FijdC1vjn/dhw3l8nJwLq9PYOBJUKhjXDR/fpOhZkUJ+Zwfuqp9SlDDo/yfFa6j2L+F1g== - dependencies: - "@aws-sdk/credential-provider-env" "3.257.0" - "@aws-sdk/credential-provider-imds" "3.259.0" - "@aws-sdk/credential-provider-process" "3.257.0" - "@aws-sdk/credential-provider-sso" "3.261.0" - "@aws-sdk/credential-provider-web-identity" "3.257.0" - "@aws-sdk/property-provider" "3.257.0" - "@aws-sdk/shared-ini-file-loader" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-node@3.196.0": - version "3.196.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.196.0.tgz" - integrity sha512-PGY7pkmqgfEwTHsuUH6fGrXWri93jqKkMbhq/QJafMGtsVupfvXvE37Rl+qgjsZjRfROrEaeLw2DGrPPmVh2cg== - dependencies: - "@aws-sdk/credential-provider-env" "3.193.0" - "@aws-sdk/credential-provider-imds" "3.193.0" - "@aws-sdk/credential-provider-ini" "3.196.0" - "@aws-sdk/credential-provider-process" "3.193.0" - "@aws-sdk/credential-provider-sso" "3.196.0" - "@aws-sdk/credential-provider-web-identity" "3.193.0" - "@aws-sdk/property-provider" "3.193.0" - "@aws-sdk/shared-ini-file-loader" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-node@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.224.0.tgz" - integrity sha512-n/gijJAA3uVFl1b3+hp2E3lPaiajsPLHqH+mMxNxPkGo39HV1v9RAyOVW4Y3AH1QcT7sURevjGoF2Eemcro88g== - dependencies: - "@aws-sdk/credential-provider-env" "3.224.0" - "@aws-sdk/credential-provider-imds" "3.224.0" - "@aws-sdk/credential-provider-ini" "3.224.0" - "@aws-sdk/credential-provider-process" "3.224.0" - "@aws-sdk/credential-provider-sso" "3.224.0" - "@aws-sdk/credential-provider-web-identity" "3.224.0" - "@aws-sdk/property-provider" "3.224.0" - "@aws-sdk/shared-ini-file-loader" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-node@3.261.0": - version "3.261.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.261.0.tgz#af7587b7d284556626e718e6345f0f40c509237e" - integrity sha512-7T25a7jbHsXPe7XvIekzhR50b7PTlISKqHdE8LNVUSzFQbSjVXulFk3vyQVIhmt5HKNkSBcMPDr6hKrSl7OLBw== - dependencies: - "@aws-sdk/credential-provider-env" "3.257.0" - "@aws-sdk/credential-provider-imds" "3.259.0" - "@aws-sdk/credential-provider-ini" "3.261.0" - "@aws-sdk/credential-provider-process" "3.257.0" - "@aws-sdk/credential-provider-sso" "3.261.0" - "@aws-sdk/credential-provider-web-identity" "3.257.0" - "@aws-sdk/property-provider" "3.257.0" - "@aws-sdk/shared-ini-file-loader" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-process@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.193.0.tgz" - integrity sha512-zpXxtQzQqkaUuFqmHW9dSkh9p/1k+XNKlwEkG8FTwAJNUWmy2ZMJv+8NTVn4s4vaRu7xJ1er9chspYr7mvxHlA== - dependencies: - "@aws-sdk/property-provider" "3.193.0" - "@aws-sdk/shared-ini-file-loader" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-process@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.224.0.tgz" - integrity sha512-0nc8vGmv6vDfFlVyKREwAa4namfuGqKg3TTM0nW2vE10fpDXZM/DGVAs5HInX+27QQNLVVh3/OHHgti9wMkYkw== - dependencies: - "@aws-sdk/property-provider" "3.224.0" - "@aws-sdk/shared-ini-file-loader" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-process@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.257.0.tgz#7fd27f48606ad7c2af375b168c8e38dc938e3162" - integrity sha512-xK8uYeNXaclaBCGrLi4z2pxPRngqLf5BM5jg2fn57zqvlL9V5gJF972FehrVBL0bfp1/laG0ZJtD2K2sapyWAw== - dependencies: - "@aws-sdk/property-provider" "3.257.0" - "@aws-sdk/shared-ini-file-loader" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-sso@3.196.0": - version "3.196.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.196.0.tgz" - integrity sha512-hJV4LDVfvPfj5zC0ysHx3zkwwJOyF+BaMGaMzaScrHyijv5e3qZzdoBLbOQFmrqVnt7DjCU02NvRSS8amLpmSw== - dependencies: - "@aws-sdk/client-sso" "3.196.0" - "@aws-sdk/property-provider" "3.193.0" - "@aws-sdk/shared-ini-file-loader" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-sso@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.224.0.tgz" - integrity sha512-Qx5w8MCGAwT5cqimA3ZgtY1jSrC7QGPzZfNflY75PWQIaYgjUNNqdAW0jipr4M/dgVjvo1j/Ek+atNf/niTOsQ== - dependencies: - "@aws-sdk/client-sso" "3.224.0" - "@aws-sdk/property-provider" "3.224.0" - "@aws-sdk/shared-ini-file-loader" "3.224.0" - "@aws-sdk/token-providers" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-sso@3.261.0": - version "3.261.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.261.0.tgz#6265828dad45b1ef67c43f712ddbcfc80e2c6fab" - integrity sha512-Ofj7m85/RuxcZMtghhD+U2GGszrU5tB2kxXcnkcHCudOER6bcOOEXnSfmdZnIv4xG+vma3VFwiWk2JkQo5zB5w== - dependencies: - "@aws-sdk/client-sso" "3.261.0" - "@aws-sdk/property-provider" "3.257.0" - "@aws-sdk/shared-ini-file-loader" "3.257.0" - "@aws-sdk/token-providers" "3.261.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-web-identity@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.193.0.tgz" - integrity sha512-MIQY9KwLCBnRyIt7an4EtMrFQZz2HC1E8vQDdKVzmeQBBePhW61fnX9XDP9bfc3Ypg1NggLG00KBPEC88twLFg== - dependencies: - "@aws-sdk/property-provider" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-web-identity@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.224.0.tgz" - integrity sha512-Z/xRFTm9pBVyuIAkYohisb3KPJowPVng7ZuZiblU0PaESoJBTkhAFOblpPv/ZWwb6fT85ANUKrvl4858zLpk/Q== - dependencies: - "@aws-sdk/property-provider" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/credential-provider-web-identity@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.257.0.tgz#928f3234818c6acbf67bf157e4a366f920285e62" - integrity sha512-Cm0uvRv4JuIbD0Kp3W0J/vwjADIyCx8HoZi5yg+QIi5nilocuTQ3ajvLeuPVSvFvdy+yaxSc5FxNXquWt7Mngw== - dependencies: - "@aws-sdk/property-provider" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/eventstream-codec@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/eventstream-codec/-/eventstream-codec-3.224.0.tgz" - integrity sha512-p8DePCwvgrrlYK7r3euI5aX/VVxrCl+DClHy0TV6/Eq8WCgWqYfZ5TSl5kbrxIc4U7pDlNIBkTiQMIl/ilEiQg== - dependencies: - "@aws-crypto/crc32" "2.0.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/util-hex-encoding" "3.201.0" - tslib "^2.3.1" - -"@aws-sdk/eventstream-serde-browser@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.224.0.tgz" - integrity sha512-QeyGmKipZsbVkezI5OKe0Xad7u1JPkZWNm1m7uqjd9vTK3A+/fw7eNxOWYVdSKs/kHyAWr9PG+fASBtr3gesPA== - dependencies: - "@aws-sdk/eventstream-serde-universal" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/eventstream-serde-config-resolver@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.224.0.tgz" - integrity sha512-zl8YUa+JZV9Dj304pc2HovMuUsz3qzo8HHj+FjIHxVsNfFL4U/NX/eDhkiWNUwVMlQIWvjksoJZ75kIzDyWGKQ== - dependencies: - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/eventstream-serde-node@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/eventstream-serde-node/-/eventstream-serde-node-3.224.0.tgz" - integrity sha512-o0PXQwyyqeBk+kkn9wIPVIdzwp28EmRt2UqEH/UO6XzpmZTghuY4ZWkQTx9n+vMZ0e/EbqIlh2BPAhELwYzMug== - dependencies: - "@aws-sdk/eventstream-serde-universal" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/eventstream-serde-universal@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/eventstream-serde-universal/-/eventstream-serde-universal-3.224.0.tgz" - integrity sha512-sI9WKnaKfpVamLCESHDOg8SkMtkjjYX3awny5PJC3/Jx9zOFN9AnvGtnIJrOGFxs5kBmQNj7c4sKCAPiTCcITw== - dependencies: - "@aws-sdk/eventstream-codec" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/fetch-http-handler@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.193.0.tgz" - integrity sha512-UhIS2LtCK9hqBzYVon6BI8WebJW1KC0GGIL/Gse5bqzU9iAGgFLAe66qg9k+/h3Jjc5LNAYzqXNVizMwn7689Q== - dependencies: - "@aws-sdk/protocol-http" "3.193.0" - "@aws-sdk/querystring-builder" "3.193.0" - "@aws-sdk/types" "3.193.0" - "@aws-sdk/util-base64-browser" "3.188.0" - tslib "^2.3.1" - -"@aws-sdk/fetch-http-handler@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.224.0.tgz" - integrity sha512-IO1Je6ZM0fwT5YYPwQwwXcD4LlsYmP52pwit8AAI4ppz6AkSfs0747uDK0DYnqls7sevBQzUSqBSt6XjcMKjYQ== - dependencies: - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/querystring-builder" "3.224.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/util-base64" "3.208.0" - tslib "^2.3.1" - -"@aws-sdk/fetch-http-handler@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.257.0.tgz#0b384ad33a57479f340ba558920a3eedded82131" - integrity sha512-zOF+RzQ+wfF7tq7tGUdPcqUTh3+k2f8KCVJE07A8kCopVq4nBu4NH6Eq29Tjpwdya3YlKvE+kFssuQRRRRex+Q== - dependencies: - "@aws-sdk/protocol-http" "3.257.0" - "@aws-sdk/querystring-builder" "3.257.0" - "@aws-sdk/types" "3.257.0" - "@aws-sdk/util-base64" "3.208.0" - tslib "^2.3.1" - -"@aws-sdk/hash-blob-browser@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/hash-blob-browser/-/hash-blob-browser-3.224.0.tgz" - integrity sha512-nUBRZzxbq6mU8FIK6OizC5jIeRkVn5tB2ZYxPd7P2IDhh1OVod6gXkq9EKTf3kecNvYgWUVHcOezZF1qLMDLHg== - dependencies: - "@aws-sdk/chunked-blob-reader" "3.188.0" - "@aws-sdk/chunked-blob-reader-native" "3.208.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/hash-node@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.193.0.tgz" - integrity sha512-O2SLPVBjrCUo+4ouAdRUoHBYsyurO9LcjNZNYD7YQOotBTbVFA3cx7kTZu+K4B6kX7FDaGbqbE1C/T1/eg/r+w== - dependencies: - "@aws-sdk/types" "3.193.0" - "@aws-sdk/util-buffer-from" "3.188.0" - tslib "^2.3.1" - -"@aws-sdk/hash-node@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.224.0.tgz" - integrity sha512-y7TXMDOSy5E2VZPvmsvRfyXkcQWcjTLFTd85yc70AAeFZiffff1nvZifQSzD78bW6ELJsWHXA2O8yxdBURyoBg== - dependencies: - "@aws-sdk/types" "3.224.0" - "@aws-sdk/util-buffer-from" "3.208.0" - tslib "^2.3.1" - -"@aws-sdk/hash-node@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/hash-node/-/hash-node-3.257.0.tgz#517e4c3c957586c0f35f916fd5c8c9841292f01f" - integrity sha512-W/USUuea5Ep3OJ2U7Ve8/5KN1YsDun2WzOFUxc1PyxXP5pW6OgC15/op0e+bmWPG851clvp5S8ZuroUr3aKi3Q== - dependencies: - "@aws-sdk/types" "3.257.0" - "@aws-sdk/util-buffer-from" "3.208.0" - "@aws-sdk/util-utf8" "3.254.0" - tslib "^2.3.1" - -"@aws-sdk/hash-stream-node@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/hash-stream-node/-/hash-stream-node-3.224.0.tgz" - integrity sha512-5RDwzB2C4Zjn4M2kZYntkc2LJdqe8CH9xmudu3ZYESkZToN5Rd3JyqobW9KPbm//R43VR4ml2qUhYHFzK6jvgg== - dependencies: - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/invalid-dependency@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.193.0.tgz" - integrity sha512-54DCknekLwJAI1os76XJ8XCzfAH7BGkBGtlWk5WCNkZTfj3rf5RUiXz4uoKUMWE1rZmyMDoDDS1PBo+yTVKW5w== - dependencies: - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/invalid-dependency@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.224.0.tgz" - integrity sha512-6huV8LBYQYx84uMhQ2SS7nqEkhTkAufwhKceXnysrcrLDuUmyth09Y7fcFblFIDTr4wTgSI0mf6DKVF4nqYCwQ== - dependencies: - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/invalid-dependency@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/invalid-dependency/-/invalid-dependency-3.257.0.tgz#e4cb2c7be40aa061dff32b0dc70db966da0938eb" - integrity sha512-T68SAPRNMEhpke0wlxURgogL7q0B8dfqZsSeS20BVR/lksJxLse9+pbmCDxiu1RrXoEIsEwl5rbLN+Hw8BFFYw== - dependencies: - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/is-array-buffer@3.188.0": - version "3.188.0" - resolved "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.188.0.tgz" - integrity sha512-n69N4zJZCNd87Rf4NzufPzhactUeM877Y0Tp/F3KiHqGeTnVjYUa4Lv1vLBjqtfjYb2HWT3NKlYn5yzrhaEwiQ== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/is-array-buffer@3.201.0": - version "3.201.0" - resolved "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.201.0.tgz" - integrity sha512-UPez5qLh3dNgt0DYnPD/q0mVJY84rA17QE26hVNOW3fAji8W2wrwrxdacWOxyXvlxWsVRcKmr+lay1MDqpAMfg== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/lib-storage@^3.226.0": - version "3.226.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/lib-storage/-/lib-storage-3.226.0.tgz#205b17e952136741ba58ed8a5cb43653e9984758" - integrity sha512-pTPQlZqYhonkaSpdD582fKKfUtQv+80vcyJdmAelUC4hZIyT98XT0wzZLp5N8etAFAgVj7Lxh59qxPB4Qz8MCw== - dependencies: - "@aws-sdk/middleware-endpoint" "3.226.0" - "@aws-sdk/smithy-client" "3.226.0" - buffer "5.6.0" - events "3.3.0" - stream-browserify "3.0.0" - tslib "^2.3.1" - -"@aws-sdk/md5-js@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/md5-js/-/md5-js-3.224.0.tgz" - integrity sha512-DT9hKzBYJUcPvGxTXwoug5Ac4zJ7q5pwOVF/PFCsN3TiXHHfDAIA0/GJjA6pZwPEi/qVy0iNhGKQK8/0i5JeWw== - dependencies: - "@aws-sdk/types" "3.224.0" - "@aws-sdk/util-utf8-browser" "3.188.0" - "@aws-sdk/util-utf8-node" "3.208.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-bucket-endpoint@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.224.0.tgz" - integrity sha512-cAmrSmVjBCENM9ojUBRhIsuQ2mPH4WxnqE5wxloHdP8BD7usNE/dMtGMhot3Dnf8WZEFpTMfhtrZrmSTCaANTQ== - dependencies: - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/util-arn-parser" "3.208.0" - "@aws-sdk/util-config-provider" "3.208.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-content-length@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.193.0.tgz" - integrity sha512-em0Sqo7O7DFOcVXU460pbcYuIjblDTZqK2YE62nQ0T+5Nbj+MSjuoite+rRRdRww9VqBkUROGKON45bUNjogtQ== - dependencies: - "@aws-sdk/protocol-http" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-content-length@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.224.0.tgz" - integrity sha512-L9b84b7X/BH+sFZaXg5hQQv0TRqZIGuOIiWJ8CkYeju7OQV03DzbCoNCAgZdI28SSevfrrVK/hwjEQrv+A6x1Q== - dependencies: - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-content-length@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-content-length/-/middleware-content-length-3.257.0.tgz#b84274ccdfca70068ce8526a197ab00359404a9a" - integrity sha512-yiawbV2azm6QnMY1L2ypG8PDRdjOcEIvFmT0T7y0F49rfbKJOu21j1ONAoCkLrINK6kMqcD5JSQLVCoURxiTxQ== - dependencies: - "@aws-sdk/protocol-http" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-endpoint@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.193.0.tgz" - integrity sha512-Inbpt7jcHGvzF7UOJOCxx9wih0+eAQYERikokidWJa7M405EJpVYq1mGbeOcQUPANU3uWF1AObmUUFhbkriHQw== - dependencies: - "@aws-sdk/middleware-serde" "3.193.0" - "@aws-sdk/protocol-http" "3.193.0" - "@aws-sdk/signature-v4" "3.193.0" - "@aws-sdk/types" "3.193.0" - "@aws-sdk/url-parser" "3.193.0" - "@aws-sdk/util-config-provider" "3.188.0" - "@aws-sdk/util-middleware" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-endpoint@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.224.0.tgz" - integrity sha512-Y+FkQmRyhQUX1E1tviodFwTrfAVjgteoALkFgIb7bxT7fmyQ/AQvdAytkDqIApTgkR61niNDSsAu7lHekDxQgg== - dependencies: - "@aws-sdk/middleware-serde" "3.224.0" - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/signature-v4" "3.224.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/url-parser" "3.224.0" - "@aws-sdk/util-config-provider" "3.208.0" - "@aws-sdk/util-middleware" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-endpoint@3.226.0": - version "3.226.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.226.0.tgz#d776480be4b5a9534c2805b7425be05497f840b7" - integrity sha512-EvLFafjtUxTT0AC9p3aBQu1/fjhWdIeK58jIXaNFONfZ3F8QbEYUPuF/SqZvJM6cWfOO9qwYKkRDbCSTYhprIg== - dependencies: - "@aws-sdk/middleware-serde" "3.226.0" - "@aws-sdk/protocol-http" "3.226.0" - "@aws-sdk/signature-v4" "3.226.0" - "@aws-sdk/types" "3.226.0" - "@aws-sdk/url-parser" "3.226.0" - "@aws-sdk/util-config-provider" "3.208.0" - "@aws-sdk/util-middleware" "3.226.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-endpoint@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.257.0.tgz#425ee4ab43807b34957685d782c84fd418a2526f" - integrity sha512-RQNQe/jeVuWZtXXfcOm+e3qMFICY6ERsXUrbt0rjHgvajZCklcrRJgxJSCwrcS7Le3nl9azFPMAMj9L7uSK28g== - dependencies: - "@aws-sdk/middleware-serde" "3.257.0" - "@aws-sdk/protocol-http" "3.257.0" - "@aws-sdk/signature-v4" "3.257.0" - "@aws-sdk/types" "3.257.0" - "@aws-sdk/url-parser" "3.257.0" - "@aws-sdk/util-config-provider" "3.208.0" - "@aws-sdk/util-middleware" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-expect-continue@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.224.0.tgz" - integrity sha512-xgihNtu5dXzRqL0QrOuMLmSoji7BsKJ+rCXjW+X+Z1flYFV5UDY5PI0dgAlgWQDWZDyu17n4R5IIZUzb/aAI1g== - dependencies: - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-flexible-checksums@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.224.0.tgz" - integrity sha512-8umP3a1YNg5+sowQgzKNiq//vSVC53iTBzg8/oszstwIMYE9aNf4RKd/X/H9biBF/G05xdTjqNAQrAh54UbKrQ== - dependencies: - "@aws-crypto/crc32" "2.0.0" - "@aws-crypto/crc32c" "2.0.0" - "@aws-sdk/is-array-buffer" "3.201.0" - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-host-header@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.193.0.tgz" - integrity sha512-aegzj5oRWd//lmfmkzRmgG2b4l3140v8Ey4QkqCxcowvAEX5a7rh23yuKaGtmiePwv2RQalCKz+tN6JXCm8g6Q== - dependencies: - "@aws-sdk/protocol-http" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-host-header@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.224.0.tgz" - integrity sha512-4eL8EVhgxTjvdVs+P3SSEkoMXBte7hSQ/+kOZVNR5ze8QPnUiDpJMS2BQrMoA2INxX9tSqp6zTrDNMc3LNvKbQ== - dependencies: - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-host-header@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.257.0.tgz#75d2ddb8073f901961665070d69c5ff3736fabdc" - integrity sha512-gEi9AJdJfRfU8Qr6HK1hfhxTzyV3Giq4B/h7um99hIFAT/GCg9xiPvAOKPo6UeuiKEv3b7RpSL4s6cBvnJMJBA== - dependencies: - "@aws-sdk/protocol-http" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-location-constraint@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.224.0.tgz" - integrity sha512-FpgKNGzImgmHTbz4Hjc41GEH4/dASxz6sTtn5T+kFDsT1j7o21tpWlS6psoazTz9Yi3ichBo2yzYUaY3QxOFew== - dependencies: - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-logger@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.193.0.tgz" - integrity sha512-D/h1pU5tAcyJpJ8ZeD1Sta0S9QZPcxERYRBiJdEl8VUrYwfy3Cl1WJedVOmd5nG73ZLRSyHeXHewb/ohge3yKQ== - dependencies: - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-logger@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.224.0.tgz" - integrity sha512-AmvuezI1vGgKZDsA2slHZJ6nQMqogUyzK27wM03458a2JgFqZvWCUPSY/P+OZ0FpnFEC34/kvvF4bI54T0C5jA== - dependencies: - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-logger@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.257.0.tgz#db35e776fe3561d0602fa39d6c69d68ee4ab36ca" - integrity sha512-8RDXW/VbMKBsXDfcCLmROZcWKyrekyiPa3J1aIaBy0tq9o4xpGoXw/lwwIrNVvISAFslb57rteup34bfn6ta6w== - dependencies: - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-recursion-detection@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.193.0.tgz" - integrity sha512-fMWP76Q1GOb/9OzS1arizm6Dbfo02DPZ6xp7OoAN3PS6ybH3Eb47s/gP3jzgBPAITQacFj4St/4a06YWYrN3NA== - dependencies: - "@aws-sdk/protocol-http" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-recursion-detection@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.224.0.tgz" - integrity sha512-ySTGlMvNaH5J77jYVVgwOF1ozz3Kp6f/wjTvivOcBR1zlRv0FXa1y033QMnrAAtKSNkzClXtNOycBM463QImJw== - dependencies: - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-recursion-detection@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.257.0.tgz#83512e0228b41dfc37a337d2ad064cf6dc41f8df" - integrity sha512-rUCih6zHh8k9Edf5N5Er4s508FYbwLM0MWTD2axzlj9TjLqEQ9OKED3wHaLffXSDzodd3oTAfJCLPbWQyoZ3ZQ== - dependencies: - "@aws-sdk/protocol-http" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-retry@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.193.0.tgz" - integrity sha512-zTQkHLBQBJi6ns655WYcYLyLPc1tgbEYU080Oc8zlveLUqoDn1ogkcmNhG7XMeQuBvWZBYN7J3/wFaXlDzeCKg== - dependencies: - "@aws-sdk/protocol-http" "3.193.0" - "@aws-sdk/service-error-classification" "3.193.0" - "@aws-sdk/types" "3.193.0" - "@aws-sdk/util-middleware" "3.193.0" - tslib "^2.3.1" - uuid "^8.3.2" - -"@aws-sdk/middleware-retry@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.224.0.tgz" - integrity sha512-zwl8rZZb5OWLzOnEW58RRklbehDfcdtD98qtgm0NLM9ErBALEEb2Y4MM5zhRiMtVjzrDw71+Mhk5+4TAlwJyXA== - dependencies: - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/service-error-classification" "3.224.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/util-middleware" "3.224.0" - tslib "^2.3.1" - uuid "^8.3.2" - -"@aws-sdk/middleware-retry@3.259.0": - version "3.259.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-retry/-/middleware-retry-3.259.0.tgz#18bbb2cd655fff1ea155dfcb9eaa2b583b67e42e" - integrity sha512-pVh1g8e84MAi7eVtWLiiiCtn82LzxOP7+LxTRHatmgIeN22yGQBZILliPDJypUPvDYlwxI1ekiK+oPTcte0Uww== - dependencies: - "@aws-sdk/protocol-http" "3.257.0" - "@aws-sdk/service-error-classification" "3.257.0" - "@aws-sdk/types" "3.257.0" - "@aws-sdk/util-middleware" "3.257.0" - "@aws-sdk/util-retry" "3.257.0" - tslib "^2.3.1" - uuid "^8.3.2" - -"@aws-sdk/middleware-sdk-s3@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.224.0.tgz" - integrity sha512-SDyFandByU9UBQOxqFk8TCE0e9FPA/nr0FRjANxkIm24/zxk2yZbk3OUx/Zr7ibo28b5BqcQV69IClBOukPiEw== - dependencies: - "@aws-sdk/middleware-bucket-endpoint" "3.224.0" - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/util-arn-parser" "3.208.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-sdk-sts@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.193.0.tgz" - integrity sha512-TafiDkeflUsnbNa89TLkDnAiRRp1gAaZLDAjt75AzriRKZnhtFfYUXWb+qAuN50T+CkJ/gZI9LHDZL5ogz/HxQ== - dependencies: - "@aws-sdk/middleware-signing" "3.193.0" - "@aws-sdk/property-provider" "3.193.0" - "@aws-sdk/protocol-http" "3.193.0" - "@aws-sdk/signature-v4" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-sdk-sts@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.224.0.tgz" - integrity sha512-rUoPPejj4N8S+P39ap9Iqbprl9L7LBlkuMHwMCqgeRJBhdI+1YeDfUekegJxceJv/BDXaoI2aSE0tCUS8rK0Ug== - dependencies: - "@aws-sdk/middleware-signing" "3.224.0" - "@aws-sdk/property-provider" "3.224.0" - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/signature-v4" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-sdk-sts@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.257.0.tgz#9cfbe9e8846c9053a40e32bc695f4bd735afeae2" - integrity sha512-d6IJCLRi3O2tm4AFK60WNhIwmMmspj1WzKR1q1TaoPzoREPG2xg+Am18wZBRkCyYuRPPrbizmkvAmAJiUolMAw== - dependencies: - "@aws-sdk/middleware-signing" "3.257.0" - "@aws-sdk/property-provider" "3.257.0" - "@aws-sdk/protocol-http" "3.257.0" - "@aws-sdk/signature-v4" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-serde@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.193.0.tgz" - integrity sha512-dH93EJYVztY+ZDPzSMRi9LfAZfKO+luH62raNy49hlNa4jiyE1Tc/+qwlmOEpfGsrtcZ9TgsON1uFF9sgBXXaA== - dependencies: - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-serde@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.224.0.tgz" - integrity sha512-4wHJ4DyhvyqQ853zfIw6sRw909VB+hFEqatmXYvO5OYap03Eed92wslsR2Gtfw1B2/zjDscPpwPyHoCIk30sHA== - dependencies: - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-serde@3.226.0": - version "3.226.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.226.0.tgz#c837ef33b34bec2af19a1c177a0c02a1ae20da5e" - integrity sha512-nPuOOAkSfx9TxzdKFx0X2bDlinOxGrqD7iof926K/AEflxGD1DBdcaDdjlYlPDW2CVE8LV/rAgbYuLxh/E/1VA== - dependencies: - "@aws-sdk/types" "3.226.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-serde@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.257.0.tgz#13c529b942dafffcb198d9333f8f8dc2a662c187" - integrity sha512-/JasfXPWFq24mnCrx9fxW/ISBSp07RJwhsF14qzm8Qy3Z0z470C+QRM6otTwAkYuuVt1wuLjja5agq3Jtzq7dQ== - dependencies: - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-signing@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.193.0.tgz" - integrity sha512-obBoELGPf5ikvHYZwbzllLeuODiokdDfe92Ve2ufeOa/d8+xsmbqNzNdCTLNNTmr1tEIaEE7ngZVTOiHqAVhyw== - dependencies: - "@aws-sdk/property-provider" "3.193.0" - "@aws-sdk/protocol-http" "3.193.0" - "@aws-sdk/signature-v4" "3.193.0" - "@aws-sdk/types" "3.193.0" - "@aws-sdk/util-middleware" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-signing@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.224.0.tgz" - integrity sha512-6T+dybVn5EYsxkNc4eVKAeoj6x6FfRXkZWMRxkepDoOJufMUNTfpoDEl6PcgJU6Wq4odbqV737x/3j53VZc6dA== - dependencies: - "@aws-sdk/property-provider" "3.224.0" - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/signature-v4" "3.224.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/util-middleware" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-signing@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.257.0.tgz#436c9e2fbbe1342c30572028e90ac62f7e90548f" - integrity sha512-hCH3D83LHmm6nqmtNrGTWZCVjsQXrGHIXbd17/qrw7aPFvcAhsiiCncGFP+XsUXEKa2ZqcSNMUyPrx69ofNRZQ== - dependencies: - "@aws-sdk/property-provider" "3.257.0" - "@aws-sdk/protocol-http" "3.257.0" - "@aws-sdk/signature-v4" "3.257.0" - "@aws-sdk/types" "3.257.0" - "@aws-sdk/util-middleware" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-ssec@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.224.0.tgz" - integrity sha512-S9a3fvF0Lv/NnXKbh0cbqhzfVcCOU1pPeGKuDB/p7AWCoql/KSG52MGBU6jKcevCtWVUKpSkgJfs+xkKmSiXIA== - dependencies: - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-stack@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.193.0.tgz" - integrity sha512-Ix5d7gE6bZwFNIVf0dGnjYuymz1gjitNoAZDPpv1nEZlUMek/jcno5lmzWFzUZXY/azpbIyaPwq/wm/c69au5A== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/middleware-stack@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.224.0.tgz" - integrity sha512-8mBrc3nj4h6FnDWnxbjfFXUPr/7UIAaGAG15D27Z/KNFnMjOqNTtpkbcoh3QQHRLX3PjTuvzT5WCqXmgD2/oiw== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/middleware-stack@3.226.0": - version "3.226.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-stack/-/middleware-stack-3.226.0.tgz#b0408370270188103987c457c758f9cf7651754f" - integrity sha512-85wF29LvPvpoed60fZGDYLwv1Zpd/cM0C22WSSFPw1SSJeqO4gtFYyCg2squfT3KI6kF43IIkOCJ+L7GtryPug== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/middleware-stack@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-stack/-/middleware-stack-3.257.0.tgz#c9fdc580c5337b703f87f6ae7df283540d6f16ac" - integrity sha512-awg2F0SvwACBaw4HIObK8pQGfSqAc4Vy+YFzWSfZNVC35oRO6RsRdKHVU99lRC0LrT2Ptmfghl2DMPSrRDbvlQ== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/middleware-user-agent@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.193.0.tgz" - integrity sha512-0vT6F9NwYQK7ARUUJeHTUIUPnupsO3IbmjHSi1+clkssFlJm2UfmSGeafiWe4AYH3anATTvZEtcxX5DZT/ExbA== - dependencies: - "@aws-sdk/protocol-http" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-user-agent@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.224.0.tgz" - integrity sha512-YXHC/n8k4qeIkqFVACPmF/QfJyKSOMD1HjM7iUZmJ9yGqDRFeGgn4o2Jktd0dor7sTv6pfUDkLqspxURAsokzA== - dependencies: - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/middleware-user-agent@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.257.0.tgz#9ca650f5909bd9b55879835088760173a9d3d249" - integrity sha512-37rt75LZyD0UWpbcFuxEGqwF3DZKSixQPl7AsDe6q3KtrO5gGQB+diH5vbY0txNNYyv5IK9WMwvY73mVmoWRmw== - dependencies: - "@aws-sdk/protocol-http" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/node-config-provider@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.193.0.tgz" - integrity sha512-5RLdjQLH69ISRG8TX9klSLOpEySXxj+z9E9Em39HRvw0/rDcd8poCTADvjYIOqRVvMka0z/hm+elvUTIVn/DRw== - dependencies: - "@aws-sdk/property-provider" "3.193.0" - "@aws-sdk/shared-ini-file-loader" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/node-config-provider@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.224.0.tgz" - integrity sha512-ULv0Ao95vNEiwCreN9ZbZ5vntaGjdMLolCiyt3B2FDWbuOorZJR5QXFydPBpo4AQOh1y/S2MIUWLhz00DY364g== - dependencies: - "@aws-sdk/property-provider" "3.224.0" - "@aws-sdk/shared-ini-file-loader" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/node-config-provider@3.259.0": - version "3.259.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/node-config-provider/-/node-config-provider-3.259.0.tgz#0b522020c4a0e445b41f7150ce624b7b63e96e68" - integrity sha512-DUOqr71oonBvM6yKPdhDBmraqgXHCFrVWFw7hc5ZNxL2wS/EsbKfGPJp+C+SUgpn1upIWPNnh/bNoLAbBkcLsA== - dependencies: - "@aws-sdk/property-provider" "3.257.0" - "@aws-sdk/shared-ini-file-loader" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/node-http-handler@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.193.0.tgz" - integrity sha512-DP4BmFw64HOShgpAPEEMZedVnRmKKjHOwMEoXcnNlAkMXnYUFHiKvudYq87Q2AnSlT6OHkyMviB61gEvIk73dA== - dependencies: - "@aws-sdk/abort-controller" "3.193.0" - "@aws-sdk/protocol-http" "3.193.0" - "@aws-sdk/querystring-builder" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/node-http-handler@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.224.0.tgz" - integrity sha512-8h4jWsfVRUcJKkqZ9msSN4LhldBpXdNlMcA8ku8IVEBHf5waxqpIhupwR0uCMmV3FDINLqkf/8EwEYAODeRjrw== - dependencies: - "@aws-sdk/abort-controller" "3.224.0" - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/querystring-builder" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/node-http-handler@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/node-http-handler/-/node-http-handler-3.257.0.tgz#33e3ba0d8b0bf72a05be6c91e6b4cf90b8a7b786" - integrity sha512-8KnWHVVwaGKyTlkTU9BSOAiSovNDoagxemU2l10QqBbzUCVpljCUMUkABEGRJ1yoQCl6DJ7RtNkAyZ8Ne/E15A== - dependencies: - "@aws-sdk/abort-controller" "3.257.0" - "@aws-sdk/protocol-http" "3.257.0" - "@aws-sdk/querystring-builder" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/property-provider@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.193.0.tgz" - integrity sha512-IaDR/PdZjKlAeSq2E/6u6nkPsZF9wvhHZckwH7uumq4ocWsWXFzaT+hKpV4YZPHx9n+K2YV4Gn/bDedpz99W1Q== - dependencies: - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/property-provider@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.224.0.tgz" - integrity sha512-1F1Hepndlmj6wykNv0ynlS9YTaT3LRF/mqXhCRGLbCWSmCiaW9BUH/ddMdBZJiSw7kcPePKid5ueW84fAO/nKg== - dependencies: - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/property-provider@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/property-provider/-/property-provider-3.257.0.tgz#dd6872ace54f8fd691a15167490ab52e40306c58" - integrity sha512-3rUbRAcF0GZ5PhDiXhS4yREfZ5hOEtvYEa9S/19OdM5eoypOaLU5XnFcCKfnccSP8SkdgpJujzxOMRWNWadlAQ== - dependencies: - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/protocol-http@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.193.0.tgz" - integrity sha512-r0wbTwFJyXq0uiImI6giqG3g/RO1N/y4wwPA7qr7OC+KXJ0NkyVxIf6e7Vx8h06aM1ATtngbwJaMP59kVCp85A== - dependencies: - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/protocol-http@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.224.0.tgz" - integrity sha512-myp31UkADbktZtIZLc4cNfr5zSNVJjPReoH37NPpvgREKOGg7ZB6Lb3UyKbjzrmIv985brMOunlMgIBIJhuPIg== - dependencies: - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/protocol-http@3.226.0": - version "3.226.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/protocol-http/-/protocol-http-3.226.0.tgz#0af7bdc331508e556b722aad0cb78eefa93466e3" - integrity sha512-zWkVqiTA9RXL6y0hhfZc9bcU4DX2NI6Hw9IhQmSPeM59mdbPjJlY4bLlMr5YxywqO3yQ/ylNoAfrEzrDjlOSRg== - dependencies: - "@aws-sdk/types" "3.226.0" - tslib "^2.3.1" - -"@aws-sdk/protocol-http@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/protocol-http/-/protocol-http-3.257.0.tgz#1452ce4f6a51e24297cc39f73aa889570dddd348" - integrity sha512-xt7LGOgZIvbLS3418AYQLacOqx+mo5j4mPiIMz7f6AaUg+/fBUgESVsncKDqxbEJVwwCXSka8Ca0cntJmoeMSw== - dependencies: - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/querystring-builder@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.193.0.tgz" - integrity sha512-PRaK6649iw0UO45UjUoiUzFcOKXZb8pMjjFJpqALpEvdZT3twxqhlPXujT7GWPKrSwO4uPLNnyYEtPY82wx2vw== - dependencies: - "@aws-sdk/types" "3.193.0" - "@aws-sdk/util-uri-escape" "3.188.0" - tslib "^2.3.1" - -"@aws-sdk/querystring-builder@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.224.0.tgz" - integrity sha512-Fwzt42wWRhf04TetQPqDL03jX5W2cAkRFQewOkIRYVFV17b72z4BFhKID6bpLEtNb4YagyllCWosNg1xooDURQ== - dependencies: - "@aws-sdk/types" "3.224.0" - "@aws-sdk/util-uri-escape" "3.201.0" - tslib "^2.3.1" - -"@aws-sdk/querystring-builder@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.257.0.tgz#75e662fc451cf59763bdee52ba64b05e5cd2de0a" - integrity sha512-mZHWLP7XIkzx1GIXO5WfX/iJ+aY9TWs02RE9FkdL2+by0HEMR65L3brQTbU1mIBJ7BjaPwYH24dljUOSABX7yg== - dependencies: - "@aws-sdk/types" "3.257.0" - "@aws-sdk/util-uri-escape" "3.201.0" - tslib "^2.3.1" - -"@aws-sdk/querystring-parser@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.193.0.tgz" - integrity sha512-dGEPCe8SK4/td5dSpiaEI3SvT5eHXrbJWbLGyD4FL3n7WCGMy2xVWAB/yrgzD0GdLDjDa8L5vLVz6yT1P9i+hA== - dependencies: - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/querystring-parser@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.224.0.tgz" - integrity sha512-UIJZ76ClFtALXRIQS3Za4R76JTsjCYReSBEQ7ag7RF1jwVZLAggdfED9w3XDrN7jbaK6i+aI3Y+eFeq0sB2fcA== - dependencies: - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/querystring-parser@3.226.0": - version "3.226.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.226.0.tgz#ba6a26727c98d46c95180e6cdc463039c5e4740d" - integrity sha512-FzB+VrQ47KAFxiPt2YXrKZ8AOLZQqGTLCKHzx4bjxGmwgsjV8yIbtJiJhZLMcUQV4LtGeIY9ixIqQhGvnZHE4A== - dependencies: - "@aws-sdk/types" "3.226.0" - tslib "^2.3.1" - -"@aws-sdk/querystring-parser@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.257.0.tgz#c8614e424d7d840c01be919161f61ef85eca46af" - integrity sha512-UDrE1dEwWrWT8dG2VCrGYrPxCWOkZ1fPTPkjpkR4KZEdQDZBqU5gYZF2xPj8Nz7pjQVHFuW2wFm3XYEk56GEjg== - dependencies: - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/service-error-classification@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.193.0.tgz" - integrity sha512-bPnXVu8ErE1RfWVVQKc2TE7EuoImUi4dSPW9g80fGRzJdQNwXb636C+7OUuWvSDzmFwuBYqZza8GZjVd+rz2zQ== - -"@aws-sdk/service-error-classification@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.224.0.tgz" - integrity sha512-0bnbYtCe+vqtaGItL+1UzQPt+yZLbU8G/aIXPQUL7555jdnjnbAtczCbIcLAJUqlE/OLwRhQVGLKbau8QAdxgQ== - -"@aws-sdk/service-error-classification@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/service-error-classification/-/service-error-classification-3.257.0.tgz#a374e811ac587b9beb6e3fda77f2249570da7a8e" - integrity sha512-FAyR0XsueGkkqDtkP03cTJQk52NdQ9sZelLynmmlGPUP75LApRPvFe1riKrou6+LsDbwVNVffj6mbDfIcOhaOw== - -"@aws-sdk/shared-ini-file-loader@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.193.0.tgz" - integrity sha512-hnvZup8RSpFXfah7Rrn6+lQJnAOCO+OiDJ2R/iMgZQh475GRQpLbu3cPhCOkjB14vVLygJtW8trK/0+zKq93bQ== - dependencies: - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/shared-ini-file-loader@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.224.0.tgz" - integrity sha512-6a/XP3lRRcX5ic+bXzF2f644KERVqMx+s0JRrGsPAwTMaMiV0A7Ifl4HKggx6dnxh8j/MXUMsWMtuxt/kCu86A== - dependencies: - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/shared-ini-file-loader@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.257.0.tgz#513eee5c7ffa343bf5d91bdd73870fc5c47a4ad3" - integrity sha512-HNjC1+Wx3xHiJc+CP14GhIdVhfQGSjroAsWseRxAhONocA9Fl1ZX4hx7+sA5c9nOoMVOovi6ivJ/6lCRPTDRrQ== - dependencies: - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/signature-v4-multi-region@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.224.0.tgz" - integrity sha512-xOW8rtEH2Rcadr+CFfiISZwcbf4jPdc4OvL6OiPsv+arndOhxk+4ZaRT2xic1FrVdYQypmSToRITYlZc9N7PjQ== - dependencies: - "@aws-sdk/protocol-http" "3.224.0" - "@aws-sdk/signature-v4" "3.224.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/util-arn-parser" "3.208.0" - tslib "^2.3.1" - -"@aws-sdk/signature-v4@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.193.0.tgz" - integrity sha512-JEqqOB8wQZz6g1ERNUOIBFDFt8OJtz5G5Uh1CdkS5W66gyWnJEz/dE1hA2VTqqQwHGGEsIEV/hlzruU1lXsvFA== - dependencies: - "@aws-sdk/is-array-buffer" "3.188.0" - "@aws-sdk/types" "3.193.0" - "@aws-sdk/util-hex-encoding" "3.188.0" - "@aws-sdk/util-middleware" "3.193.0" - "@aws-sdk/util-uri-escape" "3.188.0" - tslib "^2.3.1" - -"@aws-sdk/signature-v4@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.224.0.tgz" - integrity sha512-+oq1iylYQOvdXXO7r18SEhXIZpLd3GvJhmoReX+yjvVq8mGevDAmQiw6lwFZ6748sOmH4CREWD5H9Snrj+zLMg== - dependencies: - "@aws-sdk/is-array-buffer" "3.201.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/util-hex-encoding" "3.201.0" - "@aws-sdk/util-middleware" "3.224.0" - "@aws-sdk/util-uri-escape" "3.201.0" - tslib "^2.3.1" - -"@aws-sdk/signature-v4@3.226.0": - version "3.226.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.226.0.tgz#100390b5c5b55a9b0abd05b06fceb36cfa0ecf98" - integrity sha512-/R5q5agdPd7HJB68XMzpxrNPk158EHUvkFkuRu5Qf3kkkHebEzWEBlWoVpUe6ss4rP9Tqcue6xPuaftEmhjpYw== - dependencies: - "@aws-sdk/is-array-buffer" "3.201.0" - "@aws-sdk/types" "3.226.0" - "@aws-sdk/util-hex-encoding" "3.201.0" - "@aws-sdk/util-middleware" "3.226.0" - "@aws-sdk/util-uri-escape" "3.201.0" - tslib "^2.3.1" - -"@aws-sdk/signature-v4@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.257.0.tgz#c2f0c998bfe1980ed91e0f92c311682a61de0f90" - integrity sha512-aLQQN59X/D0+ShzPD3Anj5ntdMA/RFeNLOUCDyDvremViGi6yxUS98usQ/8bG5Rq0sW2GGMdbFUFmrDvqdiqEQ== - dependencies: - "@aws-sdk/is-array-buffer" "3.201.0" - "@aws-sdk/types" "3.257.0" - "@aws-sdk/util-hex-encoding" "3.201.0" - "@aws-sdk/util-middleware" "3.257.0" - "@aws-sdk/util-uri-escape" "3.201.0" - "@aws-sdk/util-utf8" "3.254.0" - tslib "^2.3.1" - -"@aws-sdk/smithy-client@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.193.0.tgz" - integrity sha512-BY0jhfW76vyXr7ODMaKO3eyS98RSrZgOMl6DTQV9sk7eFP/MPVlG7p7nfX/CDIgPBIO1z0A0i2CVIzYur9uGgQ== - dependencies: - "@aws-sdk/middleware-stack" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/smithy-client@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.224.0.tgz" - integrity sha512-KXXzzrCBv8ewWdtm/aolZHr2f9NRZOcDutFaWXbfSptEsK50Zi9PNzB9ZVKUHyAXYjwJHb2Sl18WRrwIxH6H4g== - dependencies: - "@aws-sdk/middleware-stack" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/smithy-client@3.226.0": - version "3.226.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.226.0.tgz#d6869ca3627ca33024616c0ec3f707981e080d59" - integrity sha512-BWr1FhWSUhkSBp0TLzliD5AQBjA2Jmo9FlOOt+cBwd9BKkSGlGj+HgATYJ83Sjjg2+J6qvEZBxB78LKVHhorBw== - dependencies: - "@aws-sdk/middleware-stack" "3.226.0" - "@aws-sdk/types" "3.226.0" - tslib "^2.3.1" - -"@aws-sdk/smithy-client@3.261.0": - version "3.261.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.261.0.tgz#538096a39198cf41fa8002467536e5af1958c518" - integrity sha512-j8XQEa3caZUVFVZfhJjaskw80O/tB+IXu84HMN44N7UkXaCFHirUsNjTDztJhnVXf/gKXzIqUqprfRnOvwLtIg== - dependencies: - "@aws-sdk/middleware-stack" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/token-providers@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.224.0.tgz" - integrity sha512-cswWqA4n1v3JIALYRA8Tq/4uHcFpBg5cgi2khNHBCF/H09Hu3dynGup6Ji8cCzf3fTak4eBQipcWaWUGE0hTGw== - dependencies: - "@aws-sdk/client-sso-oidc" "3.224.0" - "@aws-sdk/property-provider" "3.224.0" - "@aws-sdk/shared-ini-file-loader" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/token-providers@3.261.0": - version "3.261.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.261.0.tgz#29144d2f3a6f15737cde69eb794e95d7ab76558f" - integrity sha512-Vi/GOnx8rPvQz5TdJJl5CwpTX6uRsSE3fzh94O4FEAIxIFtb4P5juqg92+2CJ81C7iNduB6eEeSHtwWUylypXQ== - dependencies: - "@aws-sdk/client-sso-oidc" "3.261.0" - "@aws-sdk/property-provider" "3.257.0" - "@aws-sdk/shared-ini-file-loader" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/types@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-3.193.0.tgz" - integrity sha512-LV/wcPolRZKORrcHwkH59QMCkiDR5sM+9ZtuTxvyUGG2QFW/kjoxs08fUF10OWNJMrotBI+czDc5QJRgN8BlAw== - -"@aws-sdk/types@3.224.0", "@aws-sdk/types@^3.1.0", "@aws-sdk/types@^3.110.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-3.224.0.tgz" - integrity sha512-7te9gRondKPjEebyiPYn59Kr5LZOL48HXC05TzFIN/JXwWPJbQpROBPeKd53V1aRdr3vSQhDY01a+vDOBBrEUQ== - -"@aws-sdk/types@3.226.0": - version "3.226.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.226.0.tgz#3dba2ba223fbb8ac1ebc84de0e036ce69a81d469" - integrity sha512-MmmNHrWeO4man7wpOwrAhXlevqtOV9ZLcH4RhnG5LmRce0RFOApx24HoKENfFCcOyCm5LQBlsXCqi0dZWDWU0A== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/types@3.257.0", "@aws-sdk/types@^3.222.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.257.0.tgz#4951ee3456cd9a46829516f5596c2b8a05ffe06a" - integrity sha512-LmqXuBQBGeaGi/3Rp7XiEX1B5IPO2UUfBVvu0wwGqVsmstT0SbOVDZGPmxygACbm64n+PRx3uTSDefRfoiWYZg== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/url-parser@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.193.0.tgz" - integrity sha512-hwD1koJlOu2a6GvaSbNbdo7I6a3tmrsNTZr8bCjAcbqpc5pDThcpnl/Uaz3zHmMPs92U8I6BvWoK6pH8By06qw== - dependencies: - "@aws-sdk/querystring-parser" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/url-parser@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.224.0.tgz" - integrity sha512-DGQoiOxRVq9eEbmcGF7oz/htcHxFtLlUTzKbaX1gFuh1kmhRQwJIzz6vkrMdxOgPjvUYMJuMEcYnsHolDNWbMg== - dependencies: - "@aws-sdk/querystring-parser" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/url-parser@3.226.0": - version "3.226.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.226.0.tgz#f53d1f868b27fe74aca091a799f2af56237b15a2" - integrity sha512-p5RLE0QWyP0OcTOLmFcLdVgUcUEzmEfmdrnOxyNzomcYb0p3vUagA5zfa1HVK2azsQJFBv28GfvMnba9bGhObg== - dependencies: - "@aws-sdk/querystring-parser" "3.226.0" - "@aws-sdk/types" "3.226.0" - tslib "^2.3.1" - -"@aws-sdk/url-parser@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.257.0.tgz#99b1abb302426f1b24c9777789fb0479d52d675d" - integrity sha512-Qe/AcFe/NFZHa6cN2afXEQn9ehXxh57dWGdRjfjd2lQqNV4WW1R2pl2Tm1ZJ1dwuCNLJi4NHLMk8lrD3QQ8rdg== - dependencies: - "@aws-sdk/querystring-parser" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/util-arn-parser@3.208.0": - version "3.208.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.208.0.tgz" - integrity sha512-QV4af+kscova9dv4VuHOgH8wEr/IIYHDGcnyVtkUEqahCejWr1Kuk+SBK0xMwnZY5LSycOtQ8aeqHOn9qOjZtA== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-base64-browser@3.188.0": - version "3.188.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-base64-browser/-/util-base64-browser-3.188.0.tgz" - integrity sha512-qlH+5NZBLiyKziL335BEPedYxX6j+p7KFRWXvDQox9S+s+gLCayednpK+fteOhBenCcR9fUZOVuAPScy1I8qCg== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-base64-node@3.188.0": - version "3.188.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-base64-node/-/util-base64-node-3.188.0.tgz" - integrity sha512-r1dccRsRjKq+OhVRUfqFiW3sGgZBjHbMeHLbrAs9jrOjU2PTQ8PSzAXLvX/9lmp7YjmX17Qvlsg0NCr1tbB9OA== - dependencies: - "@aws-sdk/util-buffer-from" "3.188.0" - tslib "^2.3.1" - -"@aws-sdk/util-base64@3.208.0": - version "3.208.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-base64/-/util-base64-3.208.0.tgz" - integrity sha512-PQniZph5A6N7uuEOQi+1hnMz/FSOK/8kMFyFO+4DgA1dZ5pcKcn5wiFwHkcTb/BsgVqQa3Jx0VHNnvhlS8JyTg== - dependencies: - "@aws-sdk/util-buffer-from" "3.208.0" - tslib "^2.3.1" - -"@aws-sdk/util-body-length-browser@3.188.0": - version "3.188.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.188.0.tgz" - integrity sha512-8VpnwFWXhnZ/iRSl9mTf+VKOX9wDE8QtN4bj9pBfxwf90H1X7E8T6NkiZD3k+HubYf2J94e7DbeHs7fuCPW5Qg== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-body-length-node@3.188.0": - version "3.188.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.188.0.tgz" - integrity sha512-XwqP3vxk60MKp4YDdvDeCD6BPOiG2e+/Ou4AofZOy5/toB6NKz2pFNibQIUg2+jc7mPMnGnvOW3MQEgSJ+gu/Q== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-body-length-node@3.208.0": - version "3.208.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.208.0.tgz" - integrity sha512-3zj50e5g7t/MQf53SsuuSf0hEELzMtD8RX8C76f12OSRo2Bca4FLLYHe0TZbxcfQHom8/hOaeZEyTyMogMglqg== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-buffer-from@3.188.0": - version "3.188.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.188.0.tgz" - integrity sha512-NX1WXZ8TH20IZb4jPFT2CnLKSqZWddGxtfiWxD9M47YOtq/SSQeR82fhqqVjJn4P8w2F5E28f+Du4ntg/sGcxA== - dependencies: - "@aws-sdk/is-array-buffer" "3.188.0" - tslib "^2.3.1" - -"@aws-sdk/util-buffer-from@3.208.0": - version "3.208.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.208.0.tgz" - integrity sha512-7L0XUixNEFcLUGPeBF35enCvB9Xl+K6SQsmbrPk1P3mlV9mguWSDQqbOBwY1Ir0OVbD6H/ZOQU7hI/9RtRI0Zw== - dependencies: - "@aws-sdk/is-array-buffer" "3.201.0" - tslib "^2.3.1" - -"@aws-sdk/util-config-provider@3.188.0": - version "3.188.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.188.0.tgz" - integrity sha512-LBA7tLbi7v4uvbOJhSnjJrxbcRifKK/1ZVK94JTV2MNSCCyNkFotyEI5UWDl10YKriTIUyf7o5cakpiDZ3O4xg== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-config-provider@3.208.0": - version "3.208.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.208.0.tgz" - integrity sha512-DSRqwrERUsT34ug+anlMBIFooBEGwM8GejC7q00Y/9IPrQy50KnG5PW2NiTjuLKNi7pdEOlwTSEocJE15eDZIg== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-defaults-mode-browser@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.193.0.tgz" - integrity sha512-9riQKFrSJcsNAMnPA/3ltpSxNykeO20klE/UKjxEoD7UWjxLwsPK22UJjFwMRaHoAFcZD0LU/SgPxbC0ktCYCg== - dependencies: - "@aws-sdk/property-provider" "3.193.0" - "@aws-sdk/types" "3.193.0" - bowser "^2.11.0" - tslib "^2.3.1" - -"@aws-sdk/util-defaults-mode-browser@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.224.0.tgz" - integrity sha512-umk+A/pmlbuyvDCgdndgJUa0xitcTUF7XoUt/3qDTpNbzR5Dzgdbz74BgXUAEBJ8kPP5pCo2VE1ZD7fxqYU/dQ== - dependencies: - "@aws-sdk/property-provider" "3.224.0" - "@aws-sdk/types" "3.224.0" - bowser "^2.11.0" - tslib "^2.3.1" - -"@aws-sdk/util-defaults-mode-browser@3.261.0": - version "3.261.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.261.0.tgz#ea9f43fa569887a11db289b2e77ec6e518c5f4ed" - integrity sha512-lX3X1NfzQVV6cakepGV24uRcqevlDnQ8VgaCV8dhnw1FVThueFigyoFaUA02+uRXbV9KIbNWkEvweNtm2wvyDw== - dependencies: - "@aws-sdk/property-provider" "3.257.0" - "@aws-sdk/types" "3.257.0" - bowser "^2.11.0" - tslib "^2.3.1" - -"@aws-sdk/util-defaults-mode-node@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.193.0.tgz" - integrity sha512-occQmckvPRiM4YQIZnulfKKKjykGKWloa5ByGC5gOEGlyeP9zJpfs4zc/M2kArTAt+d2r3wkBtsKe5yKSlVEhA== - dependencies: - "@aws-sdk/config-resolver" "3.193.0" - "@aws-sdk/credential-provider-imds" "3.193.0" - "@aws-sdk/node-config-provider" "3.193.0" - "@aws-sdk/property-provider" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/util-defaults-mode-node@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.224.0.tgz" - integrity sha512-ZJQJ1McbQ5Rnf5foCFAKHT8Cbwg4IbM+bb6fCkHRJFH9AXEvwc+hPtSYf0KuI7TmoZFj9WG5JOE9Ns6g7lRHSA== - dependencies: - "@aws-sdk/config-resolver" "3.224.0" - "@aws-sdk/credential-provider-imds" "3.224.0" - "@aws-sdk/node-config-provider" "3.224.0" - "@aws-sdk/property-provider" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/util-defaults-mode-node@3.261.0": - version "3.261.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.261.0.tgz#a7c09e3912a0f23e42b5c183d2a297b632014f9f" - integrity sha512-4AK6yu4bOmHSocUdbGoEHbNXB09UA58ON2HBHY4NxMBuFBAd9XB2tYiyhce+Cm+o+lHbS8oQnw0VZw16WMzzew== - dependencies: - "@aws-sdk/config-resolver" "3.259.0" - "@aws-sdk/credential-provider-imds" "3.259.0" - "@aws-sdk/node-config-provider" "3.259.0" - "@aws-sdk/property-provider" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/util-endpoints@3.196.0": - version "3.196.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.196.0.tgz" - integrity sha512-X+DOpRUy/ij49a0GQtggk09oyIQGn0mhER6PbMT69IufZPIg3D5fC5FPEp8bfsPkb70fTEYQEsj/X/rgMQJKsA== - dependencies: - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/util-endpoints@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.224.0.tgz" - integrity sha512-k5hHbk7AP/cajw5rF7wmKP39B0WQMFdxrn8dcVOHVK0FZeKbaGCEmOf3AYXrQhswR9Xo815Rqffoml9B1z3bCA== - dependencies: - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/util-endpoints@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.257.0.tgz#40cc8f67b996f8ea173f43d0e58e57ca8c244e67" - integrity sha512-3bvmRn5XGYzPPWjLuvHBKdJOb+fijnb8Ungu9bfXnTYFsng/ndHUWeHC22O/p8w3OWoRYUIMaZHxdxe27BFozg== - dependencies: - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/util-hex-encoding@3.188.0": - version "3.188.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.188.0.tgz" - integrity sha512-QyWovTtjQ2RYxqVM+STPh65owSqzuXURnfoof778spyX4iQ4z46wOge1YV2ZtwS8w5LWd9eeVvDrLu5POPYOnA== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-hex-encoding@3.201.0": - version "3.201.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.201.0.tgz" - integrity sha512-7t1vR1pVxKx0motd3X9rI3m/xNp78p3sHtP5yo4NP4ARpxyJ0fokBomY8ScaH2D/B+U5o9ARxldJUdMqyBlJcA== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-locate-window@^3.0.0": - version "3.188.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.188.0.tgz" - integrity sha512-SxobBVLZkkLSawTCfeQnhVX3Azm9O+C2dngZVe1+BqtF8+retUbVTs7OfYeWBlawVkULKF2e781lTzEHBBjCzw== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-middleware@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.193.0.tgz" - integrity sha512-+aC6pmkcGgpxaMWCH/FXTsGWl2W342oQGs1OYKGi+W8z9UguXrqamWjdkdMqgunvj9qOEG2KBMKz1FWFFZlUyA== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-middleware@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.224.0.tgz" - integrity sha512-yA20k9sJdFgs7buVilWExUSJ/Ecr5UJRNQlmgzIpBo9kh5x/N8WyB4kN5MQw5UAA1UZ+j3jmA9+YLFT/mbX3IQ== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-middleware@3.226.0": - version "3.226.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-middleware/-/util-middleware-3.226.0.tgz#7069ae96e2e00f6bb82c722e073922fb2b051ca2" - integrity sha512-B96CQnwX4gRvQdaQkdUtqvDPkrptV5+va6FVeJOocU/DbSYMAScLxtR3peMS8cnlOT6nL1Eoa42OI9AfZz1VwQ== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-middleware@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-middleware/-/util-middleware-3.257.0.tgz#b84ee6832eea9d439ff7e7a0453ea56af87b6b7a" - integrity sha512-F9ieon8B8eGVs5tyZtAIG3DZEObDvujkspho0qRbUTHUosM0ylJLsMU800fmC/uRHLRrZvb/RSp59+kNDwSAMw== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-retry@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-retry/-/util-retry-3.257.0.tgz#20454375267e120576c9f24316dad0ebc489dc4b" - integrity sha512-l9TOsOAYtZxwW3q5fQKW4rsD9t2HVaBfQ4zBamHkNTfB4vBVvCnz4oxkvSvA2MlxCA6am+K1K/oj917Tpqk53g== - dependencies: - "@aws-sdk/service-error-classification" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/util-stream-browser@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-stream-browser/-/util-stream-browser-3.224.0.tgz" - integrity sha512-JS+C8CyxVFMQ69P4QIDTrzkhseEFCVFy2YHZYlCx3M5P+L1/PQHebTETYFMmO9ThY8TRXmYZDJHv79guvV+saQ== - dependencies: - "@aws-sdk/fetch-http-handler" "3.224.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/util-base64" "3.208.0" - "@aws-sdk/util-hex-encoding" "3.201.0" - "@aws-sdk/util-utf8-browser" "3.188.0" - tslib "^2.3.1" - -"@aws-sdk/util-stream-node@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-stream-node/-/util-stream-node-3.224.0.tgz" - integrity sha512-ztvZHJJg9/BwUrKnSz3jV6T8oOdxA1MRypK2zqTdjoPU9u/8CFQ2p0gszBApMjyxCnLWo1oM5oiMwzz1ufDrlA== - dependencies: - "@aws-sdk/node-http-handler" "3.224.0" - "@aws-sdk/types" "3.224.0" - "@aws-sdk/util-buffer-from" "3.208.0" - tslib "^2.3.1" - -"@aws-sdk/util-uri-escape@3.188.0": - version "3.188.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.188.0.tgz" - integrity sha512-4Y6AYZMT483Tiuq8dxz5WHIiPNdSFPGrl6tRTo2Oi2FcwypwmFhqgEGcqxeXDUJktvaCBxeA08DLr/AemVhPCg== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-uri-escape@3.201.0": - version "3.201.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.201.0.tgz" - integrity sha512-TeTWbGx4LU2c5rx0obHeDFeO9HvwYwQtMh1yniBz00pQb6Qt6YVOETVQikRZ+XRQwEyCg/dA375UplIpiy54mA== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-user-agent-browser@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.193.0.tgz" - integrity sha512-1EkGYsUtOMEyJG/UBIR4PtmO3lVjKNoUImoMpLtEucoGbWz5RG9zFSwLevjFyFs5roUBFlxkSpTMo8xQ3aRzQg== - dependencies: - "@aws-sdk/types" "3.193.0" - bowser "^2.11.0" - tslib "^2.3.1" - -"@aws-sdk/util-user-agent-browser@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.224.0.tgz" - integrity sha512-Dm/30cLUIM1Oam4V//m9sPrXyGOKFslUXP7Mz2AlR1HelUYoreWAIe7Rx44HR6PaXyZmjW5K0ItmcJ7tCgyMpw== - dependencies: - "@aws-sdk/types" "3.224.0" - bowser "^2.11.0" - tslib "^2.3.1" - -"@aws-sdk/util-user-agent-browser@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.257.0.tgz#6fa29ab2a15bfa82ce77d77b12891109b7673fb9" - integrity sha512-YdavWK6/8Cw6mypEgysGGX/dT9p9qnzFbnN5PQsUY+JJk2Nx8fKFydjGiQ+6rWPeW17RAv9mmbboh9uPVWxVlw== - dependencies: - "@aws-sdk/types" "3.257.0" - bowser "^2.11.0" - tslib "^2.3.1" - -"@aws-sdk/util-user-agent-node@3.193.0": - version "3.193.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.193.0.tgz" - integrity sha512-G/2/1cSgsxVtREAm8Eq8Duib5PXzXknFRHuDpAxJ5++lsJMXoYMReS278KgV54cojOkAVfcODDTqmY3Av0WHhQ== - dependencies: - "@aws-sdk/node-config-provider" "3.193.0" - "@aws-sdk/types" "3.193.0" - tslib "^2.3.1" - -"@aws-sdk/util-user-agent-node@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.224.0.tgz" - integrity sha512-BTj0vPorfT7AJzv6RxJHrnAKdIHwZmGjp5TFFaCYgFkHAPsyCPceSdZUjBRW+HbiwEwKfoHOXLGjnOBSqddZKg== - dependencies: - "@aws-sdk/node-config-provider" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/util-user-agent-node@3.259.0": - version "3.259.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.259.0.tgz#61141a0d64668ebcbbb1ac3dac1f497ca9f3707e" - integrity sha512-R0VTmNs+ySDDebU98BUbsLyeIM5YmAEr9esPpy15XfSy3AWmAeru8nLlztdaLilHZzLIDzvM2t7NGk/FzZFCvA== - dependencies: - "@aws-sdk/node-config-provider" "3.259.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/util-utf8-browser@3.188.0", "@aws-sdk/util-utf8-browser@^3.0.0": - version "3.188.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.188.0.tgz" - integrity sha512-jt627x0+jE+Ydr9NwkFstg3cUvgWh56qdaqAMDsqgRlKD21md/6G226z/Qxl7lb1VEW2LlmCx43ai/37Qwcj2Q== - dependencies: - tslib "^2.3.1" - -"@aws-sdk/util-utf8-node@3.188.0": - version "3.188.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-utf8-node/-/util-utf8-node-3.188.0.tgz" - integrity sha512-hCgP4+C0Lekjpjt2zFJ2R/iHes5sBGljXa5bScOFAEkRUc0Qw0VNgTv7LpEbIOAwGmqyxBoCwBW0YHPW1DfmYQ== - dependencies: - "@aws-sdk/util-buffer-from" "3.188.0" - tslib "^2.3.1" - -"@aws-sdk/util-utf8-node@3.208.0": - version "3.208.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-utf8-node/-/util-utf8-node-3.208.0.tgz" - integrity sha512-jKY87Acv0yWBdFxx6bveagy5FYjz+dtV8IPT7ay1E2WPWH1czoIdMAkc8tSInK31T6CRnHWkLZ1qYwCbgRfERQ== - dependencies: - "@aws-sdk/util-buffer-from" "3.208.0" - tslib "^2.3.1" - -"@aws-sdk/util-utf8@3.254.0": - version "3.254.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8/-/util-utf8-3.254.0.tgz#909af9c6549833a9a9bf77004b7484bfc96b2c35" - integrity sha512-14Kso/eIt5/qfIBmhEL9L1IfyUqswjSTqO2mY7KOzUZ9SZbwn3rpxmtkhmATkRjD7XIlLKaxBkI7tU9Zjzj8Kw== - dependencies: - "@aws-sdk/util-buffer-from" "3.208.0" - tslib "^2.3.1" - -"@aws-sdk/util-waiter@3.224.0": - version "3.224.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-waiter/-/util-waiter-3.224.0.tgz" - integrity sha512-+SNItYzUSPa8PV1iWwOi3637ztlczJNa2pZ/R1nWf2N8sAmk0BXzGJISv/GKvzPDyAz+uOpT549e8P6rRLZedA== - dependencies: - "@aws-sdk/abort-controller" "3.224.0" - "@aws-sdk/types" "3.224.0" - tslib "^2.3.1" - -"@aws-sdk/util-waiter@3.257.0": - version "3.257.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/util-waiter/-/util-waiter-3.257.0.tgz#0e390f7d8be457c276b74bf8fafb78257856d187" - integrity sha512-Fr6of3EDOcXVDs5534o7VsJMXdybB0uLy2LzeFAVSwGOY3geKhIquBAiUDqCVu9B+iTldrC0rQ9NIM7ZSpPG8w== - dependencies: - "@aws-sdk/abort-controller" "3.257.0" - "@aws-sdk/types" "3.257.0" - tslib "^2.3.1" - -"@aws-sdk/xml-builder@3.201.0": - version "3.201.0" - resolved "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.201.0.tgz" - integrity sha512-brRdB1wwMgjWEnOQsv7zSUhIQuh7DEicrfslAqHop4S4FtSI3GQAShpQqgOpMTNFYcpaWKmE/Y1MJmNY7xLCnw== - dependencies: - tslib "^2.3.1" - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz" - integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== - dependencies: - "@babel/highlight" "^7.18.6" - -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8", "@babel/compat-data@^7.19.1": - version "7.19.1" - resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.1.tgz" - integrity sha512-72a9ghR0gnESIa7jBN53U32FOVCEoztyIlKaNoU05zRhEecduGK9L9c3ww7Mp06JiR+0ls0GBPFJQwwtjn9ksg== - -"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.18.6": - version "7.19.1" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.19.1.tgz" - integrity sha512-1H8VgqXme4UXCRv7/Wa1bq7RVymKOzC7znjyFM8KiEzwFqcKUKYNoQef4GhdklgNvoBXyW4gYhuBNCM5o1zImw== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.19.0" - "@babel/helper-compilation-targets" "^7.19.1" - "@babel/helper-module-transforms" "^7.19.0" - "@babel/helpers" "^7.19.0" - "@babel/parser" "^7.19.1" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.19.1" - "@babel/types" "^7.19.0" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.1" - semver "^6.3.0" - -"@babel/generator@^7.19.0", "@babel/generator@^7.7.2": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.19.0.tgz" - integrity sha512-S1ahxf1gZ2dpoiFgA+ohK9DIpz50bJ0CWs7Zlzb54Z4sG8qmdIrGrVqmy1sAtTVRb+9CU6U8VqT9L0Zj7hxHVg== - dependencies: - "@babel/types" "^7.19.0" - "@jridgewell/gen-mapping" "^0.3.2" - jsesc "^2.5.1" - -"@babel/helper-annotate-as-pure@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz" - integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz" - integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.18.6" - "@babel/types" "^7.18.9" - -"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.19.0", "@babel/helper-compilation-targets@^7.19.1": - version "7.19.1" - resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.1.tgz" - integrity sha512-LlLkkqhCMyz2lkQPvJNdIYU7O5YjWRgC2R4omjCTpZd8u8KMQzZvX4qce+/BluN1rcQiV7BoGUpmQ0LeHerbhg== - dependencies: - "@babel/compat-data" "^7.19.1" - "@babel/helper-validator-option" "^7.18.6" - browserslist "^4.21.3" - semver "^6.3.0" - -"@babel/helper-create-class-features-plugin@^7.18.6": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.19.0.tgz" - integrity sha512-NRz8DwF4jT3UfrmUoZjd0Uph9HQnP30t7Ash+weACcyNkiYTywpIjDBgReJMKgr+n86sn2nPVVmJ28Dm053Kqw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-member-expression-to-functions" "^7.18.9" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-replace-supers" "^7.18.9" - "@babel/helper-split-export-declaration" "^7.18.6" - -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.19.0": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.19.0.tgz" - integrity sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - regexpu-core "^5.1.0" - -"@babel/helper-define-polyfill-provider@^0.3.3": - version "0.3.3" - resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz" - integrity sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww== - dependencies: - "@babel/helper-compilation-targets" "^7.17.7" - "@babel/helper-plugin-utils" "^7.16.7" - debug "^4.1.1" - lodash.debounce "^4.0.8" - resolve "^1.14.2" - semver "^6.1.2" - -"@babel/helper-environment-visitor@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz" - integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== - -"@babel/helper-explode-assignable-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz" - integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz" - integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== - dependencies: - "@babel/template" "^7.18.10" - "@babel/types" "^7.19.0" - -"@babel/helper-hoist-variables@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz" - integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-member-expression-to-functions@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz" - integrity sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg== - dependencies: - "@babel/types" "^7.18.9" - -"@babel/helper-module-imports@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz" - integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.19.0": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz" - integrity sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-simple-access" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/helper-validator-identifier" "^7.18.6" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.19.0" - "@babel/types" "^7.19.0" - -"@babel/helper-optimise-call-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz" - integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz" - integrity sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw== - -"@babel/helper-remap-async-to-generator@^7.18.6", "@babel/helper-remap-async-to-generator@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz" - integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-wrap-function" "^7.18.9" - "@babel/types" "^7.18.9" - -"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.18.9": - version "7.19.1" - resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz" - integrity sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-member-expression-to-functions" "^7.18.9" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/traverse" "^7.19.1" - "@babel/types" "^7.19.0" - -"@babel/helper-simple-access@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz" - integrity sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-skip-transparent-expression-wrappers@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.9.tgz" - integrity sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw== - dependencies: - "@babel/types" "^7.18.9" - -"@babel/helper-split-export-declaration@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz" - integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-string-parser@^7.18.10": - version "7.18.10" - resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz" - integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw== - -"@babel/helper-validator-identifier@^7.18.6": - version "7.19.1" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz" - integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== - -"@babel/helper-validator-option@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz" - integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== - -"@babel/helper-wrap-function@^7.18.9": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.19.0.tgz" - integrity sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg== - dependencies: - "@babel/helper-function-name" "^7.19.0" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.19.0" - "@babel/types" "^7.19.0" - -"@babel/helpers@^7.19.0": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.0.tgz" - integrity sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg== - dependencies: - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.19.0" - "@babel/types" "^7.19.0" - -"@babel/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== - dependencies: - "@babel/helper-validator-identifier" "^7.18.6" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.19.1", "@babel/parser@^7.7.0": - version "7.19.1" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.19.1.tgz" - integrity sha512-h7RCSorm1DdTVGJf3P2Mhj3kdnkmF/EiysUkzS2TdgAYqyjFdMQJbVuXOBej2SBJaXan/lIVtT6KkGbyyq753A== - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz" - integrity sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz" - integrity sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" - "@babel/plugin-proposal-optional-chaining" "^7.18.9" - -"@babel/plugin-proposal-async-generator-functions@^7.19.1": - version "7.19.1" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.19.1.tgz" - integrity sha512-0yu8vNATgLy4ivqMNBIwb1HebCelqN7YX8SL3FDXORv/RqT0zEEWUCH4GH44JsSrvCu6GqnAdR5EBFAPeNBB4Q== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-plugin-utils" "^7.19.0" - "@babel/helper-remap-async-to-generator" "^7.18.9" - "@babel/plugin-syntax-async-generators" "^7.8.4" - -"@babel/plugin-proposal-class-properties@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz" - integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-proposal-class-static-block@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz" - integrity sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - -"@babel/plugin-proposal-dynamic-import@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz" - integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - -"@babel/plugin-proposal-export-namespace-from@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz" - integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-proposal-json-strings@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz" - integrity sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-json-strings" "^7.8.3" - -"@babel/plugin-proposal-logical-assignment-operators@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz" - integrity sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - -"@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz" - integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - -"@babel/plugin-proposal-numeric-separator@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz" - integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-proposal-object-rest-spread@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.18.9.tgz" - integrity sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q== - dependencies: - "@babel/compat-data" "^7.18.8" - "@babel/helper-compilation-targets" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.18.8" - -"@babel/plugin-proposal-optional-catch-binding@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz" - integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-proposal-optional-chaining@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz" - integrity sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-proposal-private-methods@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz" - integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-proposal-private-property-in-object@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz" - integrity sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - -"@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz" - integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-bigint@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz" - integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13", "@babel/plugin-syntax-class-properties@^7.8.3": - version "7.12.13" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-import-assertions@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.18.6.tgz" - integrity sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-syntax-import-meta@^7.8.3": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz" - integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5", "@babel/plugin-syntax-top-level-await@^7.8.3": - version "7.14.5" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-typescript@^7.7.2": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz" - integrity sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-arrow-functions@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz" - integrity sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-async-to-generator@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz" - integrity sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag== - dependencies: - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-remap-async-to-generator" "^7.18.6" - -"@babel/plugin-transform-block-scoped-functions@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz" - integrity sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-block-scoping@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.18.9.tgz" - integrity sha512-5sDIJRV1KtQVEbt/EIBwGy4T01uYIo4KRB3VUqzkhrAIOGx7AoctL9+Ux88btY0zXdDyPJ9mW+bg+v+XEkGmtw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-classes@^7.19.0": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.19.0.tgz" - integrity sha512-YfeEE9kCjqTS9IitkgfJuxjcEtLUHMqa8yUJ6zdz8vR7hKuo6mOy2C05P0F1tdMmDCeuyidKnlrw/iTppHcr2A== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-compilation-targets" "^7.19.0" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-plugin-utils" "^7.19.0" - "@babel/helper-replace-supers" "^7.18.9" - "@babel/helper-split-export-declaration" "^7.18.6" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz" - integrity sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-destructuring@^7.18.13": - version "7.18.13" - resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.13.tgz" - integrity sha512-TodpQ29XekIsex2A+YJPj5ax2plkGa8YYY6mFjCohk/IG9IY42Rtuj1FuDeemfg2ipxIFLzPeA83SIBnlhSIow== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz" - integrity sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-duplicate-keys@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz" - integrity sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-exponentiation-operator@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz" - integrity sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-for-of@^7.18.8": - version "7.18.8" - resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz" - integrity sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-function-name@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz" - integrity sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ== - dependencies: - "@babel/helper-compilation-targets" "^7.18.9" - "@babel/helper-function-name" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-literals@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz" - integrity sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-member-expression-literals@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz" - integrity sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-modules-amd@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.18.6.tgz" - integrity sha512-Pra5aXsmTsOnjM3IajS8rTaLCy++nGM4v3YR4esk5PCsyg9z8NA5oQLwxzMUtDBd8F+UmVza3VxoAaWCbzH1rg== - dependencies: - "@babel/helper-module-transforms" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-commonjs@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.18.6.tgz" - integrity sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q== - dependencies: - "@babel/helper-module-transforms" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-simple-access" "^7.18.6" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-systemjs@^7.19.0": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.0.tgz" - integrity sha512-x9aiR0WXAWmOWsqcsnrzGR+ieaTMVyGyffPVA7F8cXAGt/UxefYv6uSHZLkAFChN5M5Iy1+wjE+xJuPt22H39A== - dependencies: - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-module-transforms" "^7.19.0" - "@babel/helper-plugin-utils" "^7.19.0" - "@babel/helper-validator-identifier" "^7.18.6" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-umd@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz" - integrity sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ== - dependencies: - "@babel/helper-module-transforms" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.19.1": - version "7.19.1" - resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.19.1.tgz" - integrity sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.19.0" - "@babel/helper-plugin-utils" "^7.19.0" - -"@babel/plugin-transform-new-target@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz" - integrity sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-object-super@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz" - integrity sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-replace-supers" "^7.18.6" - -"@babel/plugin-transform-parameters@^7.18.8": - version "7.18.8" - resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.18.8.tgz" - integrity sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-property-literals@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz" - integrity sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-regenerator@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz" - integrity sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - regenerator-transform "^0.15.0" - -"@babel/plugin-transform-reserved-words@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz" - integrity sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-shorthand-properties@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz" - integrity sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-spread@^7.19.0": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz" - integrity sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w== - dependencies: - "@babel/helper-plugin-utils" "^7.19.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" - -"@babel/plugin-transform-sticky-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz" - integrity sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-template-literals@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz" - integrity sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-typeof-symbol@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz" - integrity sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-unicode-escapes@^7.18.10": - version "7.18.10" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz" - integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-unicode-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz" - integrity sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/preset-env@^7.18.6": - version "7.19.1" - resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.19.1.tgz" - integrity sha512-c8B2c6D16Lp+Nt6HcD+nHl0VbPKVnNPTpszahuxJJnurfMtKeZ80A+qUv48Y7wqvS+dTFuLuaM9oYxyNHbCLWA== - dependencies: - "@babel/compat-data" "^7.19.1" - "@babel/helper-compilation-targets" "^7.19.1" - "@babel/helper-plugin-utils" "^7.19.0" - "@babel/helper-validator-option" "^7.18.6" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.9" - "@babel/plugin-proposal-async-generator-functions" "^7.19.1" - "@babel/plugin-proposal-class-properties" "^7.18.6" - "@babel/plugin-proposal-class-static-block" "^7.18.6" - "@babel/plugin-proposal-dynamic-import" "^7.18.6" - "@babel/plugin-proposal-export-namespace-from" "^7.18.9" - "@babel/plugin-proposal-json-strings" "^7.18.6" - "@babel/plugin-proposal-logical-assignment-operators" "^7.18.9" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" - "@babel/plugin-proposal-numeric-separator" "^7.18.6" - "@babel/plugin-proposal-object-rest-spread" "^7.18.9" - "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" - "@babel/plugin-proposal-optional-chaining" "^7.18.9" - "@babel/plugin-proposal-private-methods" "^7.18.6" - "@babel/plugin-proposal-private-property-in-object" "^7.18.6" - "@babel/plugin-proposal-unicode-property-regex" "^7.18.6" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-import-assertions" "^7.18.6" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-transform-arrow-functions" "^7.18.6" - "@babel/plugin-transform-async-to-generator" "^7.18.6" - "@babel/plugin-transform-block-scoped-functions" "^7.18.6" - "@babel/plugin-transform-block-scoping" "^7.18.9" - "@babel/plugin-transform-classes" "^7.19.0" - "@babel/plugin-transform-computed-properties" "^7.18.9" - "@babel/plugin-transform-destructuring" "^7.18.13" - "@babel/plugin-transform-dotall-regex" "^7.18.6" - "@babel/plugin-transform-duplicate-keys" "^7.18.9" - "@babel/plugin-transform-exponentiation-operator" "^7.18.6" - "@babel/plugin-transform-for-of" "^7.18.8" - "@babel/plugin-transform-function-name" "^7.18.9" - "@babel/plugin-transform-literals" "^7.18.9" - "@babel/plugin-transform-member-expression-literals" "^7.18.6" - "@babel/plugin-transform-modules-amd" "^7.18.6" - "@babel/plugin-transform-modules-commonjs" "^7.18.6" - "@babel/plugin-transform-modules-systemjs" "^7.19.0" - "@babel/plugin-transform-modules-umd" "^7.18.6" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.19.1" - "@babel/plugin-transform-new-target" "^7.18.6" - "@babel/plugin-transform-object-super" "^7.18.6" - "@babel/plugin-transform-parameters" "^7.18.8" - "@babel/plugin-transform-property-literals" "^7.18.6" - "@babel/plugin-transform-regenerator" "^7.18.6" - "@babel/plugin-transform-reserved-words" "^7.18.6" - "@babel/plugin-transform-shorthand-properties" "^7.18.6" - "@babel/plugin-transform-spread" "^7.19.0" - "@babel/plugin-transform-sticky-regex" "^7.18.6" - "@babel/plugin-transform-template-literals" "^7.18.9" - "@babel/plugin-transform-typeof-symbol" "^7.18.9" - "@babel/plugin-transform-unicode-escapes" "^7.18.10" - "@babel/plugin-transform-unicode-regex" "^7.18.6" - "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.19.0" - babel-plugin-polyfill-corejs2 "^0.3.3" - babel-plugin-polyfill-corejs3 "^0.6.0" - babel-plugin-polyfill-regenerator "^0.4.1" - core-js-compat "^3.25.1" - semver "^6.3.0" - -"@babel/preset-modules@^0.1.5": - version "0.1.5" - resolved "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz" - integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-transform-dotall-regex" "^7.4.4" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/runtime@^7.8.4": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz" - integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/template@^7.18.10", "@babel/template@^7.3.3": - version "7.18.10" - resolved "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz" - integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.18.10" - "@babel/types" "^7.18.10" - -"@babel/traverse@^7.19.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.2": - version "7.19.1" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.1.tgz" - integrity sha512-0j/ZfZMxKukDaag2PtOPDbwuELqIar6lLskVPPJDjXMXjfLb1Obo/1yjxIGqqAJrmfaTIY3z2wFLAQ7qSkLsuA== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.19.0" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.19.1" - "@babel/types" "^7.19.0" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": - version "7.19.0" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz" - integrity sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA== - dependencies: - "@babel/helper-string-parser" "^7.18.10" - "@babel/helper-validator-identifier" "^7.18.6" - to-fast-properties "^2.0.0" - -"@bcoe/v8-coverage@^0.2.3": - version "0.2.3" - resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" - integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== - -"@cbor-extract/cbor-extract-darwin-arm64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.1.1.tgz#5721f6dd3feae0b96d23122853ce977e0671b7a6" - integrity sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA== - -"@cbor-extract/cbor-extract-darwin-x64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.1.1.tgz#c25e7d0133950d87d101d7b3afafea8d50d83f5f" - integrity sha512-h6KFOzqk8jXTvkOftyRIWGrd7sKQzQv2jVdTL9nKSf3D2drCvQB/LHUxAOpPXo3pv2clDtKs3xnHalpEh3rDsw== - -"@cbor-extract/cbor-extract-linux-arm64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.1.1.tgz#48f78e7d8f0fcc84ed074b6bfa6d15dd83187c63" - integrity sha512-SxAaRcYf8S0QHaMc7gvRSiTSr7nUYMqbUdErBEu+HYA4Q6UNydx1VwFE68hGcp1qvxcy9yT5U7gA+a5XikfwSQ== - -"@cbor-extract/cbor-extract-linux-arm@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.1.1.tgz#7507d346389cb682e44fab8fae9534edd52e2e41" - integrity sha512-ds0uikdcIGUjPyraV4oJqyVE5gl/qYBpa/Wnh6l6xLE2lj/hwnjT2XcZCChdXwW/YFZ1LUHs6waoYN8PmK0nKQ== - -"@cbor-extract/cbor-extract-linux-x64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.1.1.tgz#b7c1d2be61c58ec18d58afbad52411ded63cd4cd" - integrity sha512-GVK+8fNIE9lJQHAlhOROYiI0Yd4bAZ4u++C2ZjlkS3YmO6hi+FUxe6Dqm+OKWTcMpL/l71N6CQAmaRcb4zyJuA== - -"@cbor-extract/cbor-extract-win32-x64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.1.1.tgz#21b11a1a3f18c3e7d62fd5f87438b7ed2c64c1f7" - integrity sha512-2Niq1C41dCRIDeD8LddiH+mxGlO7HJ612Ll3D/E73ZWBmycued+8ghTr/Ho3CMOWPUEr08XtyBMVXAjqF+TcKw== - -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== - dependencies: - "@jridgewell/trace-mapping" "0.3.9" - -"@did-plc/lib@*", "@did-plc/lib@^0.0.1": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@did-plc/lib/-/lib-0.0.1.tgz#5fd78c71901168ac05c5650af3a376c76461991c" - integrity sha512-RkY5w9DbYMco3SjeepqIiMveqz35exjlVDipCs2gz9AXF4/cp9hvmrp9zUWEw2vny+FjV8vGEN7QpaXWaO6nhg== - dependencies: - "@atproto/common" "0.1.0" - "@atproto/crypto" "0.1.0" - "@ipld/dag-cbor" "^7.0.3" - axios "^1.3.4" - multiformats "^9.6.4" - uint8arrays "3.0.0" - zod "^3.14.2" - -"@did-plc/server@^0.0.1": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@did-plc/server/-/server-0.0.1.tgz#8d1ba701f3b2b952b7c8fe03ef3118bb0cba077c" - integrity sha512-GtxxHcOrOQ6fNI1ufq3Zqjc2PtWqPZOdsuzlwtxiH9XibUGwDkb0GmaBHyU5GiOxOKZEW1GspZ8mreBA6XOlTQ== - dependencies: - "@atproto/common" "0.1.0" - "@atproto/crypto" "0.1.0" - "@did-plc/lib" "*" - axios "^1.3.4" - cors "^2.8.5" - express "^4.18.2" - express-async-errors "^3.1.1" - http-terminator "^3.2.0" - kysely "^0.23.4" - multiformats "^9.6.4" - pg "^8.9.0" - pino "^8.11.0" - pino-http "^8.3.3" - -"@esbuild/linux-loong64@0.14.54": - version "0.14.54" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028" - integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw== - -"@eslint/eslintrc@^1.3.2": - version "1.3.2" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz" - integrity sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.4.0" - globals "^13.15.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - -"@gar/promisify@^1.0.1", "@gar/promisify@^1.1.3": - version "1.1.3" - resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz" - integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== - -"@humanwhocodes/config-array@^0.10.5": - version "0.10.6" - resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.6.tgz" - integrity sha512-U/piU+VwXZsIgwnl+N+nRK12jCpHdc3s0UAc6zc1+HUgiESJxClpvYao/x9JwaN7onNeVb7kTlxlAvuEoaJ3ig== - dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" - minimatch "^3.0.4" - -"@humanwhocodes/gitignore-to-minimatch@^1.0.2": - version "1.0.2" - resolved "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz" - integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA== - -"@humanwhocodes/module-importer@^1.0.1": - version "1.0.1" - resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" - integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== - -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== - -"@hutson/parse-repository-url@^3.0.0": - version "3.0.2" - resolved "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz" - integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== - -"@ioredis/commands@^1.1.1": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" - integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== - -"@ipld/car@^3.2.3": - version "3.2.4" - resolved "https://registry.yarnpkg.com/@ipld/car/-/car-3.2.4.tgz#115951ba2255ec51d865773a074e422c169fb01c" - integrity sha512-rezKd+jk8AsTGOoJKqzfjLJ3WVft7NZNH95f0pfPbicROvzTyvHCNy567HzSUd6gRXZ9im29z5ZEv9Hw49jSYw== - dependencies: - "@ipld/dag-cbor" "^7.0.0" - multiformats "^9.5.4" - varint "^6.0.0" - -"@ipld/dag-cbor@^7.0.0", "@ipld/dag-cbor@^7.0.3": - version "7.0.3" - resolved "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-7.0.3.tgz" - integrity sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA== - dependencies: - cborg "^1.6.0" - multiformats "^9.5.4" - -"@isaacs/ttlcache@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz#21fb23db34e9b6220c6ba023a0118a2dd3461ea2" - integrity sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA== - -"@istanbuljs/load-nyc-config@^1.0.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" - integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== - dependencies: - camelcase "^5.3.1" - find-up "^4.1.0" - get-package-type "^0.1.0" - js-yaml "^3.13.1" - resolve-from "^5.0.0" - -"@istanbuljs/schema@^0.1.2": - version "0.1.3" - resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" - integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== - -"@jest/console@^28.1.3": - version "28.1.3" - resolved "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz" - integrity sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw== - dependencies: - "@jest/types" "^28.1.3" - "@types/node" "*" - chalk "^4.0.0" - jest-message-util "^28.1.3" - jest-util "^28.1.3" - slash "^3.0.0" - -"@jest/core@^28.1.3": - version "28.1.3" - resolved "https://registry.npmjs.org/@jest/core/-/core-28.1.3.tgz" - integrity sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA== - dependencies: - "@jest/console" "^28.1.3" - "@jest/reporters" "^28.1.3" - "@jest/test-result" "^28.1.3" - "@jest/transform" "^28.1.3" - "@jest/types" "^28.1.3" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - ci-info "^3.2.0" - exit "^0.1.2" - graceful-fs "^4.2.9" - jest-changed-files "^28.1.3" - jest-config "^28.1.3" - jest-haste-map "^28.1.3" - jest-message-util "^28.1.3" - jest-regex-util "^28.0.2" - jest-resolve "^28.1.3" - jest-resolve-dependencies "^28.1.3" - jest-runner "^28.1.3" - jest-runtime "^28.1.3" - jest-snapshot "^28.1.3" - jest-util "^28.1.3" - jest-validate "^28.1.3" - jest-watcher "^28.1.3" - micromatch "^4.0.4" - pretty-format "^28.1.3" - rimraf "^3.0.0" - slash "^3.0.0" - strip-ansi "^6.0.0" - -"@jest/create-cache-key-function@^27.4.2": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/create-cache-key-function/-/create-cache-key-function-27.5.1.tgz#7448fae15602ea95c828f5eceed35c202a820b31" - integrity sha512-dmH1yW+makpTSURTy8VzdUwFnfQh1G8R+DxO2Ho2FFmBbKFEVm+3jWdvFhE2VqB/LATCTokkP0dotjyQyw5/AQ== - dependencies: - "@jest/types" "^27.5.1" - -"@jest/environment@^28.1.3": - version "28.1.3" - resolved "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz" - integrity sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA== - dependencies: - "@jest/fake-timers" "^28.1.3" - "@jest/types" "^28.1.3" - "@types/node" "*" - jest-mock "^28.1.3" - -"@jest/expect-utils@^28.1.3": - version "28.1.3" - resolved "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz" - integrity sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA== - dependencies: - jest-get-type "^28.0.2" - -"@jest/expect@^28.1.3": - version "28.1.3" - resolved "https://registry.npmjs.org/@jest/expect/-/expect-28.1.3.tgz" - integrity sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw== - dependencies: - expect "^28.1.3" - jest-snapshot "^28.1.3" - -"@jest/fake-timers@^28.1.3": - version "28.1.3" - resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.3.tgz" - integrity sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw== - dependencies: - "@jest/types" "^28.1.3" - "@sinonjs/fake-timers" "^9.1.2" - "@types/node" "*" - jest-message-util "^28.1.3" - jest-mock "^28.1.3" - jest-util "^28.1.3" - -"@jest/globals@^28.1.3": - version "28.1.3" - resolved "https://registry.npmjs.org/@jest/globals/-/globals-28.1.3.tgz" - integrity sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA== - dependencies: - "@jest/environment" "^28.1.3" - "@jest/expect" "^28.1.3" - "@jest/types" "^28.1.3" - -"@jest/reporters@^28.1.3": - version "28.1.3" - resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.3.tgz" - integrity sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg== - dependencies: - "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^28.1.3" - "@jest/test-result" "^28.1.3" - "@jest/transform" "^28.1.3" - "@jest/types" "^28.1.3" - "@jridgewell/trace-mapping" "^0.3.13" - "@types/node" "*" - chalk "^4.0.0" - collect-v8-coverage "^1.0.0" - exit "^0.1.2" - glob "^7.1.3" - graceful-fs "^4.2.9" - istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^5.1.0" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.1.3" - jest-message-util "^28.1.3" - jest-util "^28.1.3" - jest-worker "^28.1.3" - slash "^3.0.0" - string-length "^4.0.1" - strip-ansi "^6.0.0" - terminal-link "^2.0.0" - v8-to-istanbul "^9.0.1" - -"@jest/schemas@^28.1.3": - version "28.1.3" - resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz" - integrity sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg== - dependencies: - "@sinclair/typebox" "^0.24.1" - -"@jest/source-map@^28.1.2": - version "28.1.2" - resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-28.1.2.tgz" - integrity sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww== - dependencies: - "@jridgewell/trace-mapping" "^0.3.13" - callsites "^3.0.0" - graceful-fs "^4.2.9" - -"@jest/test-result@^28.1.3": - version "28.1.3" - resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz" - integrity sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg== - dependencies: - "@jest/console" "^28.1.3" - "@jest/types" "^28.1.3" - "@types/istanbul-lib-coverage" "^2.0.0" - collect-v8-coverage "^1.0.0" - -"@jest/test-sequencer@^28.1.3": - version "28.1.3" - resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.3.tgz" - integrity sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw== - dependencies: - "@jest/test-result" "^28.1.3" - graceful-fs "^4.2.9" - jest-haste-map "^28.1.3" - slash "^3.0.0" - -"@jest/transform@^28.1.3": - version "28.1.3" - resolved "https://registry.npmjs.org/@jest/transform/-/transform-28.1.3.tgz" - integrity sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA== - dependencies: - "@babel/core" "^7.11.6" - "@jest/types" "^28.1.3" - "@jridgewell/trace-mapping" "^0.3.13" - babel-plugin-istanbul "^6.1.1" - chalk "^4.0.0" - convert-source-map "^1.4.0" - fast-json-stable-stringify "^2.0.0" - graceful-fs "^4.2.9" - jest-haste-map "^28.1.3" - jest-regex-util "^28.0.2" - jest-util "^28.1.3" - micromatch "^4.0.4" - pirates "^4.0.4" - slash "^3.0.0" - write-file-atomic "^4.0.1" - -"@jest/types@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" - integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^16.0.0" - chalk "^4.0.0" - -"@jest/types@^28.1.3": - version "28.1.3" - resolved "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz" - integrity sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ== - dependencies: - "@jest/schemas" "^28.1.3" - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^17.0.8" - chalk "^4.0.0" - -"@jridgewell/gen-mapping@^0.1.0": - version "0.1.1" - resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz" - integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== - dependencies: - "@jridgewell/set-array" "^1.0.0" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jridgewell/gen-mapping@^0.3.2": - version "0.3.2" - resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz" - integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== - dependencies: - "@jridgewell/set-array" "^1.0.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/resolve-uri@^3.0.3": - version "3.1.0" - resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== - -"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== - -"@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.14" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.13", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.15" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz" - integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@lerna/add@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/add/-/add-4.0.0.tgz" - integrity sha512-cpmAH1iS3k8JBxNvnMqrGTTjbY/ZAiKa1ChJzFevMYY3eeqbvhsBKnBcxjRXtdrJ6bd3dCQM+ZtK+0i682Fhng== - dependencies: - "@lerna/bootstrap" "4.0.0" - "@lerna/command" "4.0.0" - "@lerna/filter-options" "4.0.0" - "@lerna/npm-conf" "4.0.0" - "@lerna/validation-error" "4.0.0" - dedent "^0.7.0" - npm-package-arg "^8.1.0" - p-map "^4.0.0" - pacote "^11.2.6" - semver "^7.3.4" - -"@lerna/bootstrap@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-4.0.0.tgz" - integrity sha512-RkS7UbeM2vu+kJnHzxNRCLvoOP9yGNgkzRdy4UV2hNalD7EP41bLvRVOwRYQ7fhc2QcbhnKNdOBihYRL0LcKtw== - dependencies: - "@lerna/command" "4.0.0" - "@lerna/filter-options" "4.0.0" - "@lerna/has-npm-version" "4.0.0" - "@lerna/npm-install" "4.0.0" - "@lerna/package-graph" "4.0.0" - "@lerna/pulse-till-done" "4.0.0" - "@lerna/rimraf-dir" "4.0.0" - "@lerna/run-lifecycle" "4.0.0" - "@lerna/run-topologically" "4.0.0" - "@lerna/symlink-binary" "4.0.0" - "@lerna/symlink-dependencies" "4.0.0" - "@lerna/validation-error" "4.0.0" - dedent "^0.7.0" - get-port "^5.1.1" - multimatch "^5.0.0" - npm-package-arg "^8.1.0" - npmlog "^4.1.2" - p-map "^4.0.0" - p-map-series "^2.1.0" - p-waterfall "^2.1.1" - read-package-tree "^5.3.1" - semver "^7.3.4" - -"@lerna/changed@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/changed/-/changed-4.0.0.tgz" - integrity sha512-cD+KuPRp6qiPOD+BO6S6SN5cARspIaWSOqGBpGnYzLb4uWT8Vk4JzKyYtc8ym1DIwyoFXHosXt8+GDAgR8QrgQ== - dependencies: - "@lerna/collect-updates" "4.0.0" - "@lerna/command" "4.0.0" - "@lerna/listable" "4.0.0" - "@lerna/output" "4.0.0" - -"@lerna/check-working-tree@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-4.0.0.tgz" - integrity sha512-/++bxM43jYJCshBiKP5cRlCTwSJdRSxVmcDAXM+1oUewlZJVSVlnks5eO0uLxokVFvLhHlC5kHMc7gbVFPHv6Q== - dependencies: - "@lerna/collect-uncommitted" "4.0.0" - "@lerna/describe-ref" "4.0.0" - "@lerna/validation-error" "4.0.0" - -"@lerna/child-process@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/child-process/-/child-process-4.0.0.tgz" - integrity sha512-XtCnmCT9eyVsUUHx6y/CTBYdV9g2Cr/VxyseTWBgfIur92/YKClfEtJTbOh94jRT62hlKLqSvux/UhxXVh613Q== - dependencies: - chalk "^4.1.0" - execa "^5.0.0" - strong-log-transformer "^2.1.0" - -"@lerna/clean@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/clean/-/clean-4.0.0.tgz" - integrity sha512-uugG2iN9k45ITx2jtd8nEOoAtca8hNlDCUM0N3lFgU/b1mEQYAPRkqr1qs4FLRl/Y50ZJ41wUz1eazS+d/0osA== - dependencies: - "@lerna/command" "4.0.0" - "@lerna/filter-options" "4.0.0" - "@lerna/prompt" "4.0.0" - "@lerna/pulse-till-done" "4.0.0" - "@lerna/rimraf-dir" "4.0.0" - p-map "^4.0.0" - p-map-series "^2.1.0" - p-waterfall "^2.1.1" - -"@lerna/cli@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/cli/-/cli-4.0.0.tgz" - integrity sha512-Neaw3GzFrwZiRZv2g7g6NwFjs3er1vhraIniEs0jjVLPMNC4eata0na3GfE5yibkM/9d3gZdmihhZdZ3EBdvYA== - dependencies: - "@lerna/global-options" "4.0.0" - dedent "^0.7.0" - npmlog "^4.1.2" - yargs "^16.2.0" - -"@lerna/collect-uncommitted@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/collect-uncommitted/-/collect-uncommitted-4.0.0.tgz" - integrity sha512-ufSTfHZzbx69YNj7KXQ3o66V4RC76ffOjwLX0q/ab//61bObJ41n03SiQEhSlmpP+gmFbTJ3/7pTe04AHX9m/g== - dependencies: - "@lerna/child-process" "4.0.0" - chalk "^4.1.0" - npmlog "^4.1.2" - -"@lerna/collect-updates@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-4.0.0.tgz" - integrity sha512-bnNGpaj4zuxsEkyaCZLka9s7nMs58uZoxrRIPJ+nrmrZYp1V5rrd+7/NYTuunOhY2ug1sTBvTAxj3NZQ+JKnOw== - dependencies: - "@lerna/child-process" "4.0.0" - "@lerna/describe-ref" "4.0.0" - minimatch "^3.0.4" - npmlog "^4.1.2" - slash "^3.0.0" - -"@lerna/command@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/command/-/command-4.0.0.tgz" - integrity sha512-LM9g3rt5FsPNFqIHUeRwWXLNHJ5NKzOwmVKZ8anSp4e1SPrv2HNc1V02/9QyDDZK/w+5POXH5lxZUI1CHaOK/A== - dependencies: - "@lerna/child-process" "4.0.0" - "@lerna/package-graph" "4.0.0" - "@lerna/project" "4.0.0" - "@lerna/validation-error" "4.0.0" - "@lerna/write-log-file" "4.0.0" - clone-deep "^4.0.1" - dedent "^0.7.0" - execa "^5.0.0" - is-ci "^2.0.0" - npmlog "^4.1.2" - -"@lerna/conventional-commits@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-4.0.0.tgz" - integrity sha512-CSUQRjJHFrH8eBn7+wegZLV3OrNc0Y1FehYfYGhjLE2SIfpCL4bmfu/ViYuHh9YjwHaA+4SX6d3hR+xkeseKmw== - dependencies: - "@lerna/validation-error" "4.0.0" - conventional-changelog-angular "^5.0.12" - conventional-changelog-core "^4.2.2" - conventional-recommended-bump "^6.1.0" - fs-extra "^9.1.0" - get-stream "^6.0.0" - lodash.template "^4.5.0" - npm-package-arg "^8.1.0" - npmlog "^4.1.2" - pify "^5.0.0" - semver "^7.3.4" - -"@lerna/create-symlink@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/create-symlink/-/create-symlink-4.0.0.tgz" - integrity sha512-I0phtKJJdafUiDwm7BBlEUOtogmu8+taxq6PtIrxZbllV9hWg59qkpuIsiFp+no7nfRVuaasNYHwNUhDAVQBig== - dependencies: - cmd-shim "^4.1.0" - fs-extra "^9.1.0" - npmlog "^4.1.2" - -"@lerna/create@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/create/-/create-4.0.0.tgz" - integrity sha512-mVOB1niKByEUfxlbKTM1UNECWAjwUdiioIbRQZEeEabtjCL69r9rscIsjlGyhGWCfsdAG5wfq4t47nlDXdLLag== - dependencies: - "@lerna/child-process" "4.0.0" - "@lerna/command" "4.0.0" - "@lerna/npm-conf" "4.0.0" - "@lerna/validation-error" "4.0.0" - dedent "^0.7.0" - fs-extra "^9.1.0" - globby "^11.0.2" - init-package-json "^2.0.2" - npm-package-arg "^8.1.0" - p-reduce "^2.1.0" - pacote "^11.2.6" - pify "^5.0.0" - semver "^7.3.4" - slash "^3.0.0" - validate-npm-package-license "^3.0.4" - validate-npm-package-name "^3.0.0" - whatwg-url "^8.4.0" - yargs-parser "20.2.4" - -"@lerna/describe-ref@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-4.0.0.tgz" - integrity sha512-eTU5+xC4C5Gcgz+Ey4Qiw9nV2B4JJbMulsYJMW8QjGcGh8zudib7Sduj6urgZXUYNyhYpRs+teci9M2J8u+UvQ== - dependencies: - "@lerna/child-process" "4.0.0" - npmlog "^4.1.2" - -"@lerna/diff@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/diff/-/diff-4.0.0.tgz" - integrity sha512-jYPKprQVg41+MUMxx6cwtqsNm0Yxx9GDEwdiPLwcUTFx+/qKCEwifKNJ1oGIPBxyEHX2PFCOjkK39lHoj2qiag== - dependencies: - "@lerna/child-process" "4.0.0" - "@lerna/command" "4.0.0" - "@lerna/validation-error" "4.0.0" - npmlog "^4.1.2" - -"@lerna/exec@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/exec/-/exec-4.0.0.tgz" - integrity sha512-VGXtL/b/JfY84NB98VWZpIExfhLOzy0ozm/0XaS4a2SmkAJc5CeUfrhvHxxkxiTBLkU+iVQUyYEoAT0ulQ8PCw== - dependencies: - "@lerna/child-process" "4.0.0" - "@lerna/command" "4.0.0" - "@lerna/filter-options" "4.0.0" - "@lerna/profiler" "4.0.0" - "@lerna/run-topologically" "4.0.0" - "@lerna/validation-error" "4.0.0" - p-map "^4.0.0" - -"@lerna/filter-options@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-4.0.0.tgz" - integrity sha512-vV2ANOeZhOqM0rzXnYcFFCJ/kBWy/3OA58irXih9AMTAlQLymWAK0akWybl++sUJ4HB9Hx12TOqaXbYS2NM5uw== - dependencies: - "@lerna/collect-updates" "4.0.0" - "@lerna/filter-packages" "4.0.0" - dedent "^0.7.0" - npmlog "^4.1.2" - -"@lerna/filter-packages@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-4.0.0.tgz" - integrity sha512-+4AJIkK7iIiOaqCiVTYJxh/I9qikk4XjNQLhE3kixaqgMuHl1NQ99qXRR0OZqAWB9mh8Z1HA9bM5K1HZLBTOqA== - dependencies: - "@lerna/validation-error" "4.0.0" - multimatch "^5.0.0" - npmlog "^4.1.2" - -"@lerna/get-npm-exec-opts@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-4.0.0.tgz" - integrity sha512-yvmkerU31CTWS2c7DvmAWmZVeclPBqI7gPVr5VATUKNWJ/zmVcU4PqbYoLu92I9Qc4gY1TuUplMNdNuZTSL7IQ== - dependencies: - npmlog "^4.1.2" - -"@lerna/get-packed@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/get-packed/-/get-packed-4.0.0.tgz" - integrity sha512-rfWONRsEIGyPJTxFzC8ECb3ZbsDXJbfqWYyeeQQDrJRPnEJErlltRLPLgC2QWbxFgFPsoDLeQmFHJnf0iDfd8w== - dependencies: - fs-extra "^9.1.0" - ssri "^8.0.1" - tar "^6.1.0" - -"@lerna/github-client@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/github-client/-/github-client-4.0.0.tgz" - integrity sha512-2jhsldZtTKXYUBnOm23Lb0Fx8G4qfSXF9y7UpyUgWUj+YZYd+cFxSuorwQIgk5P4XXrtVhsUesIsli+BYSThiw== - dependencies: - "@lerna/child-process" "4.0.0" - "@octokit/plugin-enterprise-rest" "^6.0.1" - "@octokit/rest" "^18.1.0" - git-url-parse "^11.4.4" - npmlog "^4.1.2" - -"@lerna/gitlab-client@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/gitlab-client/-/gitlab-client-4.0.0.tgz" - integrity sha512-OMUpGSkeDWFf7BxGHlkbb35T7YHqVFCwBPSIR6wRsszY8PAzCYahtH3IaJzEJyUg6vmZsNl0FSr3pdA2skhxqA== - dependencies: - node-fetch "^2.6.1" - npmlog "^4.1.2" - whatwg-url "^8.4.0" - -"@lerna/global-options@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/global-options/-/global-options-4.0.0.tgz" - integrity sha512-TRMR8afAHxuYBHK7F++Ogop2a82xQjoGna1dvPOY6ltj/pEx59pdgcJfYcynYqMkFIk8bhLJJN9/ndIfX29FTQ== - -"@lerna/has-npm-version@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-4.0.0.tgz" - integrity sha512-LQ3U6XFH8ZmLCsvsgq1zNDqka0Xzjq5ibVN+igAI5ccRWNaUsE/OcmsyMr50xAtNQMYMzmpw5GVLAivT2/YzCg== - dependencies: - "@lerna/child-process" "4.0.0" - semver "^7.3.4" - -"@lerna/import@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/import/-/import-4.0.0.tgz" - integrity sha512-FaIhd+4aiBousKNqC7TX1Uhe97eNKf5/SC7c5WZANVWtC7aBWdmswwDt3usrzCNpj6/Wwr9EtEbYROzxKH8ffg== - dependencies: - "@lerna/child-process" "4.0.0" - "@lerna/command" "4.0.0" - "@lerna/prompt" "4.0.0" - "@lerna/pulse-till-done" "4.0.0" - "@lerna/validation-error" "4.0.0" - dedent "^0.7.0" - fs-extra "^9.1.0" - p-map-series "^2.1.0" - -"@lerna/info@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/info/-/info-4.0.0.tgz" - integrity sha512-8Uboa12kaCSZEn4XRfPz5KU9XXoexSPS4oeYGj76s2UQb1O1GdnEyfjyNWoUl1KlJ2i/8nxUskpXIftoFYH0/Q== - dependencies: - "@lerna/command" "4.0.0" - "@lerna/output" "4.0.0" - envinfo "^7.7.4" - -"@lerna/init@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/init/-/init-4.0.0.tgz" - integrity sha512-wY6kygop0BCXupzWj5eLvTUqdR7vIAm0OgyV9WHpMYQGfs1V22jhztt8mtjCloD/O0nEe4tJhdG62XU5aYmPNQ== - dependencies: - "@lerna/child-process" "4.0.0" - "@lerna/command" "4.0.0" - fs-extra "^9.1.0" - p-map "^4.0.0" - write-json-file "^4.3.0" - -"@lerna/link@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/link/-/link-4.0.0.tgz" - integrity sha512-KlvPi7XTAcVOByfaLlOeYOfkkDcd+bejpHMCd1KcArcFTwijOwXOVi24DYomIeHvy6HsX/IUquJ4PPUJIeB4+w== - dependencies: - "@lerna/command" "4.0.0" - "@lerna/package-graph" "4.0.0" - "@lerna/symlink-dependencies" "4.0.0" - p-map "^4.0.0" - slash "^3.0.0" - -"@lerna/list@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/list/-/list-4.0.0.tgz" - integrity sha512-L2B5m3P+U4Bif5PultR4TI+KtW+SArwq1i75QZ78mRYxPc0U/piau1DbLOmwrdqr99wzM49t0Dlvl6twd7GHFg== - dependencies: - "@lerna/command" "4.0.0" - "@lerna/filter-options" "4.0.0" - "@lerna/listable" "4.0.0" - "@lerna/output" "4.0.0" - -"@lerna/listable@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/listable/-/listable-4.0.0.tgz" - integrity sha512-/rPOSDKsOHs5/PBLINZOkRIX1joOXUXEtyUs5DHLM8q6/RP668x/1lFhw6Dx7/U+L0+tbkpGtZ1Yt0LewCLgeQ== - dependencies: - "@lerna/query-graph" "4.0.0" - chalk "^4.1.0" - columnify "^1.5.4" - -"@lerna/log-packed@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/log-packed/-/log-packed-4.0.0.tgz" - integrity sha512-+dpCiWbdzgMAtpajLToy9PO713IHoE6GV/aizXycAyA07QlqnkpaBNZ8DW84gHdM1j79TWockGJo9PybVhrrZQ== - dependencies: - byte-size "^7.0.0" - columnify "^1.5.4" - has-unicode "^2.0.1" - npmlog "^4.1.2" - -"@lerna/npm-conf@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/npm-conf/-/npm-conf-4.0.0.tgz" - integrity sha512-uS7H02yQNq3oejgjxAxqq/jhwGEE0W0ntr8vM3EfpCW1F/wZruwQw+7bleJQ9vUBjmdXST//tk8mXzr5+JXCfw== - dependencies: - config-chain "^1.1.12" - pify "^5.0.0" - -"@lerna/npm-dist-tag@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-4.0.0.tgz" - integrity sha512-F20sg28FMYTgXqEQihgoqSfwmq+Id3zT23CnOwD+XQMPSy9IzyLf1fFVH319vXIw6NF6Pgs4JZN2Qty6/CQXGw== - dependencies: - "@lerna/otplease" "4.0.0" - npm-package-arg "^8.1.0" - npm-registry-fetch "^9.0.0" - npmlog "^4.1.2" - -"@lerna/npm-install@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-4.0.0.tgz" - integrity sha512-aKNxq2j3bCH3eXl3Fmu4D54s/YLL9WSwV8W7X2O25r98wzrO38AUN6AB9EtmAx+LV/SP15et7Yueg9vSaanRWg== - dependencies: - "@lerna/child-process" "4.0.0" - "@lerna/get-npm-exec-opts" "4.0.0" - fs-extra "^9.1.0" - npm-package-arg "^8.1.0" - npmlog "^4.1.2" - signal-exit "^3.0.3" - write-pkg "^4.0.0" - -"@lerna/npm-publish@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-4.0.0.tgz" - integrity sha512-vQb7yAPRo5G5r77DRjHITc9piR9gvEKWrmfCH7wkfBnGWEqu7n8/4bFQ7lhnkujvc8RXOsYpvbMQkNfkYibD/w== - dependencies: - "@lerna/otplease" "4.0.0" - "@lerna/run-lifecycle" "4.0.0" - fs-extra "^9.1.0" - libnpmpublish "^4.0.0" - npm-package-arg "^8.1.0" - npmlog "^4.1.2" - pify "^5.0.0" - read-package-json "^3.0.0" - -"@lerna/npm-run-script@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-4.0.0.tgz" - integrity sha512-Jmyh9/IwXJjOXqKfIgtxi0bxi1pUeKe5bD3S81tkcy+kyng/GNj9WSqD5ZggoNP2NP//s4CLDAtUYLdP7CU9rA== - dependencies: - "@lerna/child-process" "4.0.0" - "@lerna/get-npm-exec-opts" "4.0.0" - npmlog "^4.1.2" - -"@lerna/otplease@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/otplease/-/otplease-4.0.0.tgz" - integrity sha512-Sgzbqdk1GH4psNiT6hk+BhjOfIr/5KhGBk86CEfHNJTk9BK4aZYyJD4lpDbDdMjIV4g03G7pYoqHzH765T4fxw== - dependencies: - "@lerna/prompt" "4.0.0" - -"@lerna/output@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/output/-/output-4.0.0.tgz" - integrity sha512-Un1sHtO1AD7buDQrpnaYTi2EG6sLF+KOPEAMxeUYG5qG3khTs2Zgzq5WE3dt2N/bKh7naESt20JjIW6tBELP0w== - dependencies: - npmlog "^4.1.2" - -"@lerna/pack-directory@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/pack-directory/-/pack-directory-4.0.0.tgz" - integrity sha512-NJrmZNmBHS+5aM+T8N6FVbaKFScVqKlQFJNY2k7nsJ/uklNKsLLl6VhTQBPwMTbf6Tf7l6bcKzpy7aePuq9UiQ== - dependencies: - "@lerna/get-packed" "4.0.0" - "@lerna/package" "4.0.0" - "@lerna/run-lifecycle" "4.0.0" - npm-packlist "^2.1.4" - npmlog "^4.1.2" - tar "^6.1.0" - temp-write "^4.0.0" - -"@lerna/package-graph@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-4.0.0.tgz" - integrity sha512-QED2ZCTkfXMKFoTGoccwUzjHtZMSf3UKX14A4/kYyBms9xfFsesCZ6SLI5YeySEgcul8iuIWfQFZqRw+Qrjraw== - dependencies: - "@lerna/prerelease-id-from-version" "4.0.0" - "@lerna/validation-error" "4.0.0" - npm-package-arg "^8.1.0" - npmlog "^4.1.2" - semver "^7.3.4" - -"@lerna/package@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/package/-/package-4.0.0.tgz" - integrity sha512-l0M/izok6FlyyitxiQKr+gZLVFnvxRQdNhzmQ6nRnN9dvBJWn+IxxpM+cLqGACatTnyo9LDzNTOj2Db3+s0s8Q== - dependencies: - load-json-file "^6.2.0" - npm-package-arg "^8.1.0" - write-pkg "^4.0.0" - -"@lerna/prerelease-id-from-version@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/prerelease-id-from-version/-/prerelease-id-from-version-4.0.0.tgz" - integrity sha512-GQqguzETdsYRxOSmdFZ6zDBXDErIETWOqomLERRY54f4p+tk4aJjoVdd9xKwehC9TBfIFvlRbL1V9uQGHh1opg== - dependencies: - semver "^7.3.4" - -"@lerna/profiler@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/profiler/-/profiler-4.0.0.tgz" - integrity sha512-/BaEbqnVh1LgW/+qz8wCuI+obzi5/vRE8nlhjPzdEzdmWmZXuCKyWSEzAyHOJWw1ntwMiww5dZHhFQABuoFz9Q== - dependencies: - fs-extra "^9.1.0" - npmlog "^4.1.2" - upath "^2.0.1" - -"@lerna/project@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/project/-/project-4.0.0.tgz" - integrity sha512-o0MlVbDkD5qRPkFKlBZsXZjoNTWPyuL58564nSfZJ6JYNmgAptnWPB2dQlAc7HWRZkmnC2fCkEdoU+jioPavbg== - dependencies: - "@lerna/package" "4.0.0" - "@lerna/validation-error" "4.0.0" - cosmiconfig "^7.0.0" - dedent "^0.7.0" - dot-prop "^6.0.1" - glob-parent "^5.1.1" - globby "^11.0.2" - load-json-file "^6.2.0" - npmlog "^4.1.2" - p-map "^4.0.0" - resolve-from "^5.0.0" - write-json-file "^4.3.0" - -"@lerna/prompt@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/prompt/-/prompt-4.0.0.tgz" - integrity sha512-4Ig46oCH1TH5M7YyTt53fT6TuaKMgqUUaqdgxvp6HP6jtdak6+amcsqB8YGz2eQnw/sdxunx84DfI9XpoLj4bQ== - dependencies: - inquirer "^7.3.3" - npmlog "^4.1.2" - -"@lerna/publish@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/publish/-/publish-4.0.0.tgz" - integrity sha512-K8jpqjHrChH22qtkytA5GRKIVFEtqBF6JWj1I8dWZtHs4Jywn8yB1jQ3BAMLhqmDJjWJtRck0KXhQQKzDK2UPg== - dependencies: - "@lerna/check-working-tree" "4.0.0" - "@lerna/child-process" "4.0.0" - "@lerna/collect-updates" "4.0.0" - "@lerna/command" "4.0.0" - "@lerna/describe-ref" "4.0.0" - "@lerna/log-packed" "4.0.0" - "@lerna/npm-conf" "4.0.0" - "@lerna/npm-dist-tag" "4.0.0" - "@lerna/npm-publish" "4.0.0" - "@lerna/otplease" "4.0.0" - "@lerna/output" "4.0.0" - "@lerna/pack-directory" "4.0.0" - "@lerna/prerelease-id-from-version" "4.0.0" - "@lerna/prompt" "4.0.0" - "@lerna/pulse-till-done" "4.0.0" - "@lerna/run-lifecycle" "4.0.0" - "@lerna/run-topologically" "4.0.0" - "@lerna/validation-error" "4.0.0" - "@lerna/version" "4.0.0" - fs-extra "^9.1.0" - libnpmaccess "^4.0.1" - npm-package-arg "^8.1.0" - npm-registry-fetch "^9.0.0" - npmlog "^4.1.2" - p-map "^4.0.0" - p-pipe "^3.1.0" - pacote "^11.2.6" - semver "^7.3.4" - -"@lerna/pulse-till-done@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/pulse-till-done/-/pulse-till-done-4.0.0.tgz" - integrity sha512-Frb4F7QGckaybRhbF7aosLsJ5e9WuH7h0KUkjlzSByVycxY91UZgaEIVjS2oN9wQLrheLMHl6SiFY0/Pvo0Cxg== - dependencies: - npmlog "^4.1.2" - -"@lerna/query-graph@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/query-graph/-/query-graph-4.0.0.tgz" - integrity sha512-YlP6yI3tM4WbBmL9GCmNDoeQyzcyg1e4W96y/PKMZa5GbyUvkS2+Jc2kwPD+5KcXou3wQZxSPzR3Te5OenaDdg== - dependencies: - "@lerna/package-graph" "4.0.0" - -"@lerna/resolve-symlink@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/resolve-symlink/-/resolve-symlink-4.0.0.tgz" - integrity sha512-RtX8VEUzqT+uLSCohx8zgmjc6zjyRlh6i/helxtZTMmc4+6O4FS9q5LJas2uGO2wKvBlhcD6siibGt7dIC3xZA== - dependencies: - fs-extra "^9.1.0" - npmlog "^4.1.2" - read-cmd-shim "^2.0.0" - -"@lerna/rimraf-dir@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-4.0.0.tgz" - integrity sha512-QNH9ABWk9mcMJh2/muD9iYWBk1oQd40y6oH+f3wwmVGKYU5YJD//+zMiBI13jxZRtwBx0vmBZzkBkK1dR11cBg== - dependencies: - "@lerna/child-process" "4.0.0" - npmlog "^4.1.2" - path-exists "^4.0.0" - rimraf "^3.0.2" - -"@lerna/run-lifecycle@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/run-lifecycle/-/run-lifecycle-4.0.0.tgz" - integrity sha512-IwxxsajjCQQEJAeAaxF8QdEixfI7eLKNm4GHhXHrgBu185JcwScFZrj9Bs+PFKxwb+gNLR4iI5rpUdY8Y0UdGQ== - dependencies: - "@lerna/npm-conf" "4.0.0" - npm-lifecycle "^3.1.5" - npmlog "^4.1.2" - -"@lerna/run-topologically@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/run-topologically/-/run-topologically-4.0.0.tgz" - integrity sha512-EVZw9hGwo+5yp+VL94+NXRYisqgAlj0jWKWtAIynDCpghRxCE5GMO3xrQLmQgqkpUl9ZxQFpICgYv5DW4DksQA== - dependencies: - "@lerna/query-graph" "4.0.0" - p-queue "^6.6.2" - -"@lerna/run@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/run/-/run-4.0.0.tgz" - integrity sha512-9giulCOzlMPzcZS/6Eov6pxE9gNTyaXk0Man+iCIdGJNMrCnW7Dme0Z229WWP/UoxDKg71F2tMsVVGDiRd8fFQ== - dependencies: - "@lerna/command" "4.0.0" - "@lerna/filter-options" "4.0.0" - "@lerna/npm-run-script" "4.0.0" - "@lerna/output" "4.0.0" - "@lerna/profiler" "4.0.0" - "@lerna/run-topologically" "4.0.0" - "@lerna/timer" "4.0.0" - "@lerna/validation-error" "4.0.0" - p-map "^4.0.0" - -"@lerna/symlink-binary@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-4.0.0.tgz" - integrity sha512-zualodWC4q1QQc1pkz969hcFeWXOsVYZC5AWVtAPTDfLl+TwM7eG/O6oP+Rr3fFowspxo6b1TQ6sYfDV6HXNWA== - dependencies: - "@lerna/create-symlink" "4.0.0" - "@lerna/package" "4.0.0" - fs-extra "^9.1.0" - p-map "^4.0.0" - -"@lerna/symlink-dependencies@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-4.0.0.tgz" - integrity sha512-BABo0MjeUHNAe2FNGty1eantWp8u83BHSeIMPDxNq0MuW2K3CiQRaeWT3EGPAzXpGt0+hVzBrA6+OT0GPn7Yuw== - dependencies: - "@lerna/create-symlink" "4.0.0" - "@lerna/resolve-symlink" "4.0.0" - "@lerna/symlink-binary" "4.0.0" - fs-extra "^9.1.0" - p-map "^4.0.0" - p-map-series "^2.1.0" - -"@lerna/timer@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/timer/-/timer-4.0.0.tgz" - integrity sha512-WFsnlaE7SdOvjuyd05oKt8Leg3ENHICnvX3uYKKdByA+S3g+TCz38JsNs7OUZVt+ba63nC2nbXDlUnuT2Xbsfg== - -"@lerna/validation-error@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-4.0.0.tgz" - integrity sha512-1rBOM5/koiVWlRi3V6dB863E1YzJS8v41UtsHgMr6gB2ncJ2LsQtMKlJpi3voqcgh41H8UsPXR58RrrpPpufyw== - dependencies: - npmlog "^4.1.2" - -"@lerna/version@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/version/-/version-4.0.0.tgz" - integrity sha512-otUgiqs5W9zGWJZSCCMRV/2Zm2A9q9JwSDS7s/tlKq4mWCYriWo7+wsHEA/nPTMDyYyBO5oyZDj+3X50KDUzeA== - dependencies: - "@lerna/check-working-tree" "4.0.0" - "@lerna/child-process" "4.0.0" - "@lerna/collect-updates" "4.0.0" - "@lerna/command" "4.0.0" - "@lerna/conventional-commits" "4.0.0" - "@lerna/github-client" "4.0.0" - "@lerna/gitlab-client" "4.0.0" - "@lerna/output" "4.0.0" - "@lerna/prerelease-id-from-version" "4.0.0" - "@lerna/prompt" "4.0.0" - "@lerna/run-lifecycle" "4.0.0" - "@lerna/run-topologically" "4.0.0" - "@lerna/validation-error" "4.0.0" - chalk "^4.1.0" - dedent "^0.7.0" - load-json-file "^6.2.0" - minimatch "^3.0.4" - npmlog "^4.1.2" - p-map "^4.0.0" - p-pipe "^3.1.0" - p-reduce "^2.1.0" - p-waterfall "^2.1.1" - semver "^7.3.4" - slash "^3.0.0" - temp-write "^4.0.0" - write-json-file "^4.3.0" - -"@lerna/write-log-file@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@lerna/write-log-file/-/write-log-file-4.0.0.tgz" - integrity sha512-XRG5BloiArpXRakcnPHmEHJp+4AtnhRtpDIHSghmXD5EichI1uD73J7FgPp30mm2pDRq3FdqB0NbwSEsJ9xFQg== - dependencies: - npmlog "^4.1.2" - write-file-atomic "^3.0.3" - -"@noble/curves@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" - integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA== - dependencies: - "@noble/hashes" "1.3.1" - -"@noble/hashes@1.3.1", "@noble/hashes@^1.3.1": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" - integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== - -"@noble/secp256k1@^1.7.0": - version "1.7.0" - resolved "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.0.tgz" - integrity sha512-kbacwGSsH/CTout0ZnZWxnW1B+jH/7r/WAAKLBtrRJ/+CUH7lgmQzl3GTrQua3SGKWNSDsS6lmjnDpIJ5Dxyaw== - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@npmcli/ci-detect@^1.0.0": - version "1.4.0" - resolved "https://registry.npmjs.org/@npmcli/ci-detect/-/ci-detect-1.4.0.tgz" - integrity sha512-3BGrt6FLjqM6br5AhWRKTr3u5GIVkjRYeAFrMp3HjnfICrg4xOrVRwFavKT6tsp++bq5dluL5t8ME/Nha/6c1Q== - -"@npmcli/fs@^1.0.0": - version "1.1.1" - resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz" - integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== - dependencies: - "@gar/promisify" "^1.0.1" - semver "^7.3.5" - -"@npmcli/fs@^2.1.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-2.1.2.tgz#a9e2541a4a2fec2e69c29b35e6060973da79b865" - integrity sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ== - dependencies: - "@gar/promisify" "^1.1.3" - semver "^7.3.5" - -"@npmcli/git@^2.1.0": - version "2.1.0" - resolved "https://registry.npmjs.org/@npmcli/git/-/git-2.1.0.tgz" - integrity sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw== - dependencies: - "@npmcli/promise-spawn" "^1.3.2" - lru-cache "^6.0.0" - mkdirp "^1.0.4" - npm-pick-manifest "^6.1.1" - promise-inflight "^1.0.1" - promise-retry "^2.0.1" - semver "^7.3.5" - which "^2.0.2" - -"@npmcli/installed-package-contents@^1.0.6": - version "1.0.7" - resolved "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz" - integrity sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw== - dependencies: - npm-bundled "^1.1.1" - npm-normalize-package-bin "^1.0.1" - -"@npmcli/move-file@^1.0.1": - version "1.1.2" - resolved "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz" - integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== - dependencies: - mkdirp "^1.0.4" - rimraf "^3.0.2" - -"@npmcli/move-file@^2.0.0": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-2.0.1.tgz#26f6bdc379d87f75e55739bab89db525b06100e4" - integrity sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ== - dependencies: - mkdirp "^1.0.4" - rimraf "^3.0.2" - -"@npmcli/node-gyp@^1.0.2": - version "1.0.3" - resolved "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-1.0.3.tgz" - integrity sha512-fnkhw+fmX65kiLqk6E3BFLXNC26rUhK90zVwe2yncPliVT/Qos3xjhTLE59Df8KnPlcwIERXKVlU1bXoUQ+liA== - -"@npmcli/package-json@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@npmcli/package-json/-/package-json-3.0.0.tgz" - integrity sha512-NnuPuM97xfiCpbTEJYtEuKz6CFbpUHtaT0+5via5pQeI25omvQDFbp1GcGJ/c4zvL/WX0qbde6YiLgfZbWFgvg== - dependencies: - json-parse-even-better-errors "^3.0.0" - -"@npmcli/promise-spawn@^1.2.0", "@npmcli/promise-spawn@^1.3.2": - version "1.3.2" - resolved "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz" - integrity sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg== - dependencies: - infer-owner "^1.0.4" - -"@npmcli/run-script@^1.8.2": - version "1.8.6" - resolved "https://registry.npmjs.org/@npmcli/run-script/-/run-script-1.8.6.tgz" - integrity sha512-e42bVZnC6VluBZBAFEr3YrdqSspG3bgilyg4nSLBJ7TRGNCzxHa92XAHxQBLYg0BmgwO4b2mf3h/l5EkEWRn3g== - dependencies: - "@npmcli/node-gyp" "^1.0.2" - "@npmcli/promise-spawn" "^1.3.2" - node-gyp "^7.1.0" - read-package-json-fast "^2.0.1" - -"@octokit/auth-token@^2.4.4": - version "2.5.0" - resolved "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz" - integrity sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g== - dependencies: - "@octokit/types" "^6.0.3" - -"@octokit/core@^3.5.1": - version "3.6.0" - resolved "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz" - integrity sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q== - dependencies: - "@octokit/auth-token" "^2.4.4" - "@octokit/graphql" "^4.5.8" - "@octokit/request" "^5.6.3" - "@octokit/request-error" "^2.0.5" - "@octokit/types" "^6.0.3" - before-after-hook "^2.2.0" - universal-user-agent "^6.0.0" - -"@octokit/endpoint@^6.0.1": - version "6.0.12" - resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz" - integrity sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA== - dependencies: - "@octokit/types" "^6.0.3" - is-plain-object "^5.0.0" - universal-user-agent "^6.0.0" - -"@octokit/graphql@^4.5.8": - version "4.8.0" - resolved "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz" - integrity sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg== - dependencies: - "@octokit/request" "^5.6.0" - "@octokit/types" "^6.0.3" - universal-user-agent "^6.0.0" - -"@octokit/openapi-types@^12.11.0": - version "12.11.0" - resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz" - integrity sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ== - -"@octokit/plugin-enterprise-rest@^6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz" - integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== - -"@octokit/plugin-paginate-rest@^2.16.8": - version "2.21.3" - resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz" - integrity sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw== - dependencies: - "@octokit/types" "^6.40.0" - -"@octokit/plugin-request-log@^1.0.4": - version "1.0.4" - resolved "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz" - integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== - -"@octokit/plugin-rest-endpoint-methods@^5.12.0": - version "5.16.2" - resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz" - integrity sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw== - dependencies: - "@octokit/types" "^6.39.0" - deprecation "^2.3.1" - -"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": - version "2.1.0" - resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz" - integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg== - dependencies: - "@octokit/types" "^6.0.3" - deprecation "^2.0.0" - once "^1.4.0" - -"@octokit/request@^5.6.0", "@octokit/request@^5.6.3": - version "5.6.3" - resolved "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz" - integrity sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A== - dependencies: - "@octokit/endpoint" "^6.0.1" - "@octokit/request-error" "^2.1.0" - "@octokit/types" "^6.16.1" - is-plain-object "^5.0.0" - node-fetch "^2.6.7" - universal-user-agent "^6.0.0" - -"@octokit/rest@^18.1.0": - version "18.12.0" - resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz" - integrity sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q== - dependencies: - "@octokit/core" "^3.5.1" - "@octokit/plugin-paginate-rest" "^2.16.8" - "@octokit/plugin-request-log" "^1.0.4" - "@octokit/plugin-rest-endpoint-methods" "^5.12.0" - -"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.39.0", "@octokit/types@^6.40.0": - version "6.41.0" - resolved "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz" - integrity sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg== - dependencies: - "@octokit/openapi-types" "^12.11.0" - -"@sinclair/typebox@^0.24.1": - version "0.24.42" - resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.42.tgz" - integrity sha512-d+2AtrHGyWek2u2ITF0lHRIv6Tt7X0dEHW+0rP+5aDCEjC3fiN2RBjrLD0yU0at52BcZbRGxLbAtXiR0hFCjYw== - -"@sinonjs/commons@^1.7.0": - version "1.8.3" - resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz" - integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== - dependencies: - type-detect "4.0.8" - -"@sinonjs/fake-timers@^9.1.2": - version "9.1.2" - resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz" - integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw== - dependencies: - "@sinonjs/commons" "^1.7.0" - -"@swc/core-darwin-arm64@1.3.42": - version "1.3.42" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.42.tgz#fabb645b288199b730d846e3eda370b77f5ebe9f" - integrity sha512-hM6RrZFyoCM9mX3cj/zM5oXwhAqjUdOCLXJx7KTQps7NIkv/Qjvobgvyf2gAb89j3ARNo9NdIoLjTjJ6oALtiA== - -"@swc/core-darwin-x64@1.3.42": - version "1.3.42" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.3.42.tgz#dcd434ec8dda6f2178a10da0def036a071a6e008" - integrity sha512-bjsWtHMb6wJK1+RGlBs2USvgZ0txlMk11y0qBLKo32gLKTqzUwRw0Fmfzuf6Ue2a/w//7eqMlPFEre4LvJajGw== - -"@swc/core-linux-arm-gnueabihf@1.3.42": - version "1.3.42" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.42.tgz#59c57b15113d316e8a4a6d690a6c09429483d201" - integrity sha512-Oe0ggMz3MyqXNfeVmY+bBTL0hFSNY3bx8dhcqsh4vXk/ZVGse94QoC4dd92LuPHmKT0x6nsUzB86x2jU9QHW5g== - -"@swc/core-linux-arm64-gnu@1.3.42": - version "1.3.42" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.42.tgz#50d026b9f4d7a5f25deacc8c8dd45fc12be70a95" - integrity sha512-ZJsa8NIW1RLmmHGTJCbM7OPSbBZ9rOMrLqDtUOGrT0uoJXZnnQqolflamB5wviW0X6h3Z3/PSTNGNDCJ3u3Lqg== - -"@swc/core-linux-arm64-musl@1.3.42": - version "1.3.42" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.42.tgz#3c0e51b0709dcf06289949803c9a36a46a97827c" - integrity sha512-YpZwlFAfOp5vkm/uVUJX1O7N3yJDO1fDQRWqsOPPNyIJkI2ydlRQtgN6ZylC159Qv+TimfXnGTlNr7o3iBAqjg== - -"@swc/core-linux-x64-gnu@1.3.42": - version "1.3.42" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.42.tgz#059ac0acddebd0360851871929a14dbacf74f865" - integrity sha512-0ccpKnsZbyHBzaQFdP8U9i29nvOfKitm6oJfdJzlqsY/jCqwvD8kv2CAKSK8WhJz//ExI2LqNrDI0yazx5j7+A== - -"@swc/core-linux-x64-musl@1.3.42": - version "1.3.42" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.42.tgz#7a61093d93a3abc2f893b7d31fd6c22c4cab2212" - integrity sha512-7eckRRuTZ6+3K21uyfXXgc2ZCg0mSWRRNwNT3wap2bYkKPeqTgb8pm8xYSZNEiMuDonHEat6XCCV36lFY6kOdQ== - -"@swc/core-win32-arm64-msvc@1.3.42": - version "1.3.42" - resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.42.tgz#12f92c960ea801aa26ffa5b91d369ac24c2a3cca" - integrity sha512-t27dJkdw0GWANdN4TV0lY/V5vTYSx5SRjyzzZolep358ueCGuN1XFf1R0JcCbd1ojosnkQg2L7A7991UjXingg== - -"@swc/core-win32-ia32-msvc@1.3.42": - version "1.3.42" - resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.42.tgz#be022aff03838515fa5506be300f0ea15f3fb476" - integrity sha512-xfpc/Zt/aMILX4IX0e3loZaFyrae37u3MJCv1gJxgqrpeLi7efIQr3AmERkTK3mxTO6R5urSliWw2W3FyZ7D3Q== - -"@swc/core-win32-x64-msvc@1.3.42": - version "1.3.42" - resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.42.tgz#fccac26974f03234e502276389f4330e2696887f" - integrity sha512-ra2K4Tu++EJLPhzZ6L8hWUsk94TdK/2UKhL9dzCBhtzKUixsGCEqhtqH1zISXNvW8qaVLFIMUP37ULe80/IJaA== - -"@swc/core@^1.3.42": - version "1.3.42" - resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.3.42.tgz#7067c4fd9a02536f9ca7b54ed8ebc45e2df810cf" - integrity sha512-nVFUd5+7tGniM2cT3LXaqnu3735Cu4az8A9gAKK+8sdpASI52SWuqfDBmjFCK9xG90MiVDVp2PTZr0BWqCIzpw== - optionalDependencies: - "@swc/core-darwin-arm64" "1.3.42" - "@swc/core-darwin-x64" "1.3.42" - "@swc/core-linux-arm-gnueabihf" "1.3.42" - "@swc/core-linux-arm64-gnu" "1.3.42" - "@swc/core-linux-arm64-musl" "1.3.42" - "@swc/core-linux-x64-gnu" "1.3.42" - "@swc/core-linux-x64-musl" "1.3.42" - "@swc/core-win32-arm64-msvc" "1.3.42" - "@swc/core-win32-ia32-msvc" "1.3.42" - "@swc/core-win32-x64-msvc" "1.3.42" - -"@swc/jest@^0.2.24": - version "0.2.24" - resolved "https://registry.yarnpkg.com/@swc/jest/-/jest-0.2.24.tgz#35d9377ede049613cd5fdd6c24af2b8dcf622875" - integrity sha512-fwgxQbM1wXzyKzl1+IW0aGrRvAA8k0Y3NxFhKigbPjOJ4mCKnWEcNX9HQS3gshflcxq8YKhadabGUVfdwjCr6Q== - dependencies: - "@jest/create-cache-key-function" "^27.4.2" - jsonc-parser "^3.2.0" - -"@tokenizer/token@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" - integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A== - -"@tootallnate/once@1": - version "1.1.2" - resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz" - integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== - -"@tootallnate/once@2": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" - integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== - -"@ts-morph/common@~0.17.0": - version "0.17.0" - resolved "https://registry.npmjs.org/@ts-morph/common/-/common-0.17.0.tgz" - integrity sha512-RMSSvSfs9kb0VzkvQ2NWobwnj7TxCA9vI/IjR9bDHqgAyVbu2T0DN4wiKVqomyDWqO7dPr/tErSfq7urQ1Q37g== - dependencies: - fast-glob "^3.2.11" - minimatch "^5.1.0" - mkdirp "^1.0.4" - path-browserify "^1.0.1" - -"@tsconfig/node10@^1.0.7": - version "1.0.9" - resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz" - integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== - -"@tsconfig/node12@^1.0.7": - version "1.0.11" - resolved "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz" - integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== - -"@tsconfig/node14@^1.0.0": - version "1.0.3" - resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== - -"@tsconfig/node16@^1.0.2": - version "1.0.3" - resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz" - integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== - -"@types/babel__core@^7.1.14": - version "7.1.19" - resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz" - integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - "@types/babel__generator" "*" - "@types/babel__template" "*" - "@types/babel__traverse" "*" - -"@types/babel__generator@*": - version "7.6.4" - resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz" - integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== - dependencies: - "@babel/types" "^7.0.0" - -"@types/babel__template@*": - version "7.4.1" - resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz" - integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - -"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.18.1" - resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.1.tgz" - integrity sha512-FSdLaZh2UxaMuLp9lixWaHq/golWTRWOnRsAXzDTDSDOQLuZb1nsdCt6pJSPWSEQt2eFZ2YVk3oYhn+1kLMeMA== - dependencies: - "@babel/types" "^7.3.0" - -"@types/bn.js@*": - version "5.1.1" - resolved "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.1.tgz" - integrity sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g== - dependencies: - "@types/node" "*" - -"@types/body-parser@*": - version "1.19.2" - resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz" - integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/connect@*": - version "3.4.35" - resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz" - integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== - dependencies: - "@types/node" "*" - -"@types/cors@^2.8.12": - version "2.8.12" - resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz" - integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== - -"@types/elliptic@^6.4.9": - version "6.4.14" - resolved "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.14.tgz" - integrity sha512-z4OBcDAU0GVwDTuwJzQCiL6188QvZMkvoERgcVjq0/mPM8jCfdwZ3x5zQEVoL9WCAru3aG5wl3Z5Ww5wBWn7ZQ== - dependencies: - "@types/bn.js" "*" - -"@types/express-serve-static-core@^4.17.18": - version "4.17.31" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz" - integrity sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - -"@types/express@^4.17.13": - version "4.17.14" - resolved "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz" - integrity sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.18" - "@types/qs" "*" - "@types/serve-static" "*" - -"@types/graceful-fs@^4.1.3": - version "4.1.5" - resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz" - integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== - dependencies: - "@types/node" "*" - -"@types/http-errors@^2.0.1": - version "2.0.1" - resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz" - integrity sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ== - -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": - version "2.0.4" - resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz" - integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== - -"@types/istanbul-lib-report@*": - version "3.0.0" - resolved "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" - integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== - dependencies: - "@types/istanbul-lib-coverage" "*" - -"@types/istanbul-reports@^3.0.0": - version "3.0.1" - resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz" - integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== - dependencies: - "@types/istanbul-lib-report" "*" - -"@types/jest@^28.1.4": - version "28.1.8" - resolved "https://registry.npmjs.org/@types/jest/-/jest-28.1.8.tgz" - integrity sha512-8TJkV++s7B6XqnDrzR1m/TT0A0h948Pnl/097veySPN67VRAgQ4gZ7n2KfJo2rVq6njQjdxU3GCCyDvAeuHoiw== - dependencies: - expect "^28.0.0" - pretty-format "^28.0.0" - -"@types/json-schema@^7.0.9": - version "7.0.11" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" - integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== - -"@types/jsonwebtoken@^8.5.9": - version "8.5.9" - resolved "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz" - integrity sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg== - dependencies: - "@types/node" "*" - -"@types/mime@*": - version "3.0.1" - resolved "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz" - integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== - -"@types/minimatch@^3.0.3": - version "3.0.5" - resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz" - integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== - -"@types/minimist@^1.2.0": - version "1.2.2" - resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz" - integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== - -"@types/node@*", "@types/node@^18.0.0": - version "18.11.11" - resolved "https://registry.npmjs.org/@types/node/-/node-18.11.11.tgz" - integrity sha512-KJ021B1nlQUBLopzZmPBVuGU9un7WJd/W4ya7Ih02B4Uwky5Nja0yGYav2EfYIk0RR2Q9oVhf60S2XR1BCWJ2g== - -"@types/nodemailer@^6.4.6": - version "6.4.6" - resolved "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.6.tgz" - integrity sha512-pD6fL5GQtUKvD2WnPmg5bC2e8kWCAPDwMPmHe/ohQbW+Dy0EcHgZ2oCSuPlWNqk74LS5BVMig1SymQbFMPPK3w== - dependencies: - "@types/node" "*" - -"@types/normalize-package-data@^2.4.0": - version "2.4.1" - resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz" - integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== - -"@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== - -"@types/pg@^8.6.6": - version "8.6.6" - resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.6.6.tgz#21cdf873a3e345a6e78f394677e3b3b1b543cb80" - integrity sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw== - dependencies: - "@types/node" "*" - pg-protocol "*" - pg-types "^2.2.0" - -"@types/prettier@^2.1.5": - version "2.7.0" - resolved "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.0.tgz" - integrity sha512-RI1L7N4JnW5gQw2spvL7Sllfuf1SaHdrZpCHiBlCXjIlufi1SMNnbu2teze3/QE67Fg2tBlH7W+mi4hVNk4p0A== - -"@types/qs@*": - version "6.9.7" - resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz" - integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== - -"@types/range-parser@*": - version "1.2.4" - resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz" - integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== - -"@types/serve-static@*": - version "1.15.0" - resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz" - integrity sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg== - dependencies: - "@types/mime" "*" - "@types/node" "*" - -"@types/sharp@^0.31.0": - version "0.31.0" - resolved "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.0.tgz" - integrity sha512-nwivOU101fYInCwdDcH/0/Ru6yIRXOpORx25ynEOc6/IakuCmjOAGpaO5VfUl4QkDtUC6hj+Z2eCQvgXOioknw== - dependencies: - "@types/node" "*" - -"@types/stack-utils@^2.0.0": - version "2.0.1" - resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz" - integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== - -"@types/ws@^8.5.4": - version "8.5.4" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5" - integrity sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg== - dependencies: - "@types/node" "*" - -"@types/yargs-parser@*": - version "21.0.0" - resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz" - integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== - -"@types/yargs@^16.0.0": - version "16.0.5" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.5.tgz#12cc86393985735a283e387936398c2f9e5f88e3" - integrity sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ== - dependencies: - "@types/yargs-parser" "*" - -"@types/yargs@^17.0.8": - version "17.0.12" - resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.12.tgz" - integrity sha512-Nz4MPhecOFArtm81gFQvQqdV7XYCrWKx5uUt6GNHredFHn1i2mtWqXTON7EPXMtNi1qjtjEM/VCHDhcHsAMLXQ== - dependencies: - "@types/yargs-parser" "*" - -"@typescript-eslint/eslint-plugin@^5.38.1": - version "5.38.1" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz" - integrity sha512-ky7EFzPhqz3XlhS7vPOoMDaQnQMn+9o5ICR9CPr/6bw8HrFkzhMSxuA3gRfiJVvs7geYrSeawGJjZoZQKCOglQ== - dependencies: - "@typescript-eslint/scope-manager" "5.38.1" - "@typescript-eslint/type-utils" "5.38.1" - "@typescript-eslint/utils" "5.38.1" - debug "^4.3.4" - ignore "^5.2.0" - regexpp "^3.2.0" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/parser@^5.38.1": - version "5.38.1" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.38.1.tgz" - integrity sha512-LDqxZBVFFQnQRz9rUZJhLmox+Ep5kdUmLatLQnCRR6523YV+XhRjfYzStQ4MheFA8kMAfUlclHSbu+RKdRwQKw== - dependencies: - "@typescript-eslint/scope-manager" "5.38.1" - "@typescript-eslint/types" "5.38.1" - "@typescript-eslint/typescript-estree" "5.38.1" - debug "^4.3.4" - -"@typescript-eslint/scope-manager@5.38.1": - version "5.38.1" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.38.1.tgz" - integrity sha512-BfRDq5RidVU3RbqApKmS7RFMtkyWMM50qWnDAkKgQiezRtLKsoyRKIvz1Ok5ilRWeD9IuHvaidaLxvGx/2eqTQ== - dependencies: - "@typescript-eslint/types" "5.38.1" - "@typescript-eslint/visitor-keys" "5.38.1" - -"@typescript-eslint/type-utils@5.38.1": - version "5.38.1" - resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.38.1.tgz" - integrity sha512-UU3j43TM66gYtzo15ivK2ZFoDFKKP0k03MItzLdq0zV92CeGCXRfXlfQX5ILdd4/DSpHkSjIgLLLh1NtkOJOAw== - dependencies: - "@typescript-eslint/typescript-estree" "5.38.1" - "@typescript-eslint/utils" "5.38.1" - debug "^4.3.4" - tsutils "^3.21.0" - -"@typescript-eslint/types@5.38.1": - version "5.38.1" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.38.1.tgz" - integrity sha512-QTW1iHq1Tffp9lNfbfPm4WJabbvpyaehQ0SrvVK2yfV79SytD9XDVxqiPvdrv2LK7DGSFo91TB2FgWanbJAZXg== - -"@typescript-eslint/typescript-estree@5.38.1": - version "5.38.1" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.1.tgz" - integrity sha512-99b5e/Enoe8fKMLdSuwrfH/C0EIbpUWmeEKHmQlGZb8msY33qn1KlkFww0z26o5Omx7EVjzVDCWEfrfCDHfE7g== - dependencies: - "@typescript-eslint/types" "5.38.1" - "@typescript-eslint/visitor-keys" "5.38.1" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/utils@5.38.1": - version "5.38.1" - resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.38.1.tgz" - integrity sha512-oIuUiVxPBsndrN81oP8tXnFa/+EcZ03qLqPDfSZ5xIJVm7A9V0rlkQwwBOAGtrdN70ZKDlKv+l1BeT4eSFxwXA== - dependencies: - "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.38.1" - "@typescript-eslint/types" "5.38.1" - "@typescript-eslint/typescript-estree" "5.38.1" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" - -"@typescript-eslint/visitor-keys@5.38.1": - version "5.38.1" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.1.tgz" - integrity sha512-bSHr1rRxXt54+j2n4k54p4fj8AHJ49VDWtjpImOpzQj4qjAiOpPni+V1Tyajh19Api1i844F757cur8wH3YvOA== - dependencies: - "@typescript-eslint/types" "5.38.1" - eslint-visitor-keys "^3.3.0" - -JSONStream@^1.0.4: - version "1.3.5" - resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz" - integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - -abbrev@1, abbrev@^1.0.0: - version "1.1.1" - resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - -accepts@~1.3.5, accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== - dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" - -acorn-jsx@^5.3.2: - version "5.3.2" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn-walk@^8.1.1: - version "8.2.0" - resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== - -acorn@^8.4.1, acorn@^8.8.0: - version "8.8.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz" - integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== - -add-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz" - integrity sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ== - -agent-base@6, agent-base@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -agentkeepalive@^4.1.3: - version "4.2.1" - resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz" - integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA== - dependencies: - debug "^4.1.0" - depd "^1.1.2" - humanize-ms "^1.2.1" - -agentkeepalive@^4.2.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.3.0.tgz#bb999ff07412653c1803b3ced35e50729830a255" - integrity sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg== - dependencies: - debug "^4.1.0" - depd "^2.0.0" - humanize-ms "^1.2.1" - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.3, ajv@^6.12.4: - version "6.12.6" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ansi-escapes@^4.2.1: - version "4.3.2" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" - integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-styles@^5.0.0: - version "5.2.0" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" - integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== - -anymatch@^3.0.3: - version "3.1.2" - resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -"aproba@^1.0.3 || ^2.0.0", aproba@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz" - integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== - -are-we-there-yet@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" - integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== - dependencies: - delegates "^1.0.0" - readable-stream "^3.6.0" - -are-we-there-yet@~1.1.2: - version "1.1.7" - resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz" - integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -array-differ@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz" - integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - -array-ify@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz" - integrity sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng== - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -array.prototype.reduce@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.4.tgz" - integrity sha512-WnM+AjG/DvLRLo4DDl+r+SvCzYtD2Jd9oeBYMcEaI7t3fFrHY9M53/wdLcTvmZNQ70IU6Htj0emFkZ5TS+lrdw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.2" - es-array-method-boxes-properly "^1.0.0" - is-string "^1.0.7" - -arrify@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz" - integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== - -arrify@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz" - integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== - -asap@^2.0.0: - version "2.0.6" - resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" - integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== - -asn1.js@^5.0.1: - version "5.4.1" - resolved "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - -asn1@~0.2.3: - version "0.2.6" - resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" - integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" - integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - -atomic-sleep@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz" - integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz" - integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== - -aws4@^1.8.0: - version "1.11.0" - resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== - -axios@^0.27.2: - version "0.27.2" - resolved "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz" - integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== - dependencies: - follow-redirects "^1.14.9" - form-data "^4.0.0" - -axios@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.4.tgz#f5760cefd9cfb51fd2481acf88c05f67c4523024" - integrity sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ== - dependencies: - follow-redirects "^1.15.0" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - -babel-eslint@^10.1.0: - version "10.1.0" - resolved "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz" - integrity sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.7.0" - "@babel/traverse" "^7.7.0" - "@babel/types" "^7.7.0" - eslint-visitor-keys "^1.0.0" - resolve "^1.12.0" - -babel-jest@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz" - integrity sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q== - dependencies: - "@jest/transform" "^28.1.3" - "@types/babel__core" "^7.1.14" - babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^28.1.3" - chalk "^4.0.0" - graceful-fs "^4.2.9" - slash "^3.0.0" - -babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== - dependencies: - object.assign "^4.1.0" - -babel-plugin-istanbul@^6.1.1: - version "6.1.1" - resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz" - integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@istanbuljs/load-nyc-config" "^1.0.0" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-instrument "^5.0.4" - test-exclude "^6.0.0" - -babel-plugin-jest-hoist@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz" - integrity sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q== - dependencies: - "@babel/template" "^7.3.3" - "@babel/types" "^7.3.3" - "@types/babel__core" "^7.1.14" - "@types/babel__traverse" "^7.0.6" - -babel-plugin-polyfill-corejs2@^0.3.3: - version "0.3.3" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz" - integrity sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q== - dependencies: - "@babel/compat-data" "^7.17.7" - "@babel/helper-define-polyfill-provider" "^0.3.3" - semver "^6.1.1" - -babel-plugin-polyfill-corejs3@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz" - integrity sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.3" - core-js-compat "^3.25.1" - -babel-plugin-polyfill-regenerator@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz" - integrity sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.3" - -babel-preset-current-node-syntax@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz" - integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== - dependencies: - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-bigint" "^7.8.3" - "@babel/plugin-syntax-class-properties" "^7.8.3" - "@babel/plugin-syntax-import-meta" "^7.8.3" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.8.3" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-top-level-await" "^7.8.3" - -babel-preset-jest@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz" - integrity sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A== - dependencies: - babel-plugin-jest-hoist "^28.1.3" - babel-preset-current-node-syntax "^1.0.0" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.0.2, base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" - integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== - dependencies: - tweetnacl "^0.14.3" - -before-after-hook@^2.2.0: - version "2.2.2" - resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz" - integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== - -better-sqlite3@^7.6.2: - version "7.6.2" - resolved "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-7.6.2.tgz" - integrity sha512-S5zIU1Hink2AH4xPsN0W43T1/AJ5jrPh7Oy07ocuW/AKYYY02GWzz9NH0nbSMn/gw6fDZ5jZ1QsHt1BXAwJ6Lg== - dependencies: - bindings "^1.5.0" - prebuild-install "^7.1.0" - -big-integer@^1.6.51: - version "1.6.51" - resolved "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz" - integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -bl@^4.0.3: - version "4.1.0" - resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -bn.js@^4.0.0, bn.js@^4.11.8, bn.js@^4.11.9: - version "4.12.0" - resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== - -body-parser@1.20.0: - version "1.20.0" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz" - integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== - dependencies: - bytes "3.1.2" - content-type "~1.0.4" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.10.3" - raw-body "2.5.1" - type-is "~1.6.18" - unpipe "1.0.0" - -body-parser@1.20.1: - version "1.20.1" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== - dependencies: - bytes "3.1.2" - content-type "~1.0.4" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.11.0" - raw-body "2.5.1" - type-is "~1.6.18" - unpipe "1.0.0" - -boolean@^3.1.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" - integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== - -bowser@^2.11.0: - version "2.11.0" - resolved "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz" - integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - -braces@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz" - integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== - -browserslist@^4.21.3, browserslist@^4.21.4: - version "4.21.4" - resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz" - integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== - dependencies: - caniuse-lite "^1.0.30001400" - electron-to-chromium "^1.4.251" - node-releases "^2.0.6" - update-browserslist-db "^1.0.9" - -bser@2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" - integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== - dependencies: - node-int64 "^0.4.0" - -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" - integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -buffer-writer@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz" - integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== - -buffer@5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" - integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - -buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - -builtins@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz" - integrity sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ== - -byline@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz" - integrity sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q== - -byte-size@^7.0.0: - version "7.0.1" - resolved "https://registry.npmjs.org/byte-size/-/byte-size-7.0.1.tgz" - integrity sha512-crQdqyCwhokxwV1UyDzLZanhkugAgft7vt0qbbdt60C6Zf3CAiGmtUCylbtYwrU6loOUw3euGrNtW1J651ot1A== - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== - -bytes@3.1.2, bytes@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -cacache@^15.0.5, cacache@^15.2.0: - version "15.3.0" - resolved "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz" - integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== - dependencies: - "@npmcli/fs" "^1.0.0" - "@npmcli/move-file" "^1.0.1" - chownr "^2.0.0" - fs-minipass "^2.0.0" - glob "^7.1.4" - infer-owner "^1.0.4" - lru-cache "^6.0.0" - minipass "^3.1.1" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.2" - mkdirp "^1.0.3" - p-map "^4.0.0" - promise-inflight "^1.0.1" - rimraf "^3.0.2" - ssri "^8.0.1" - tar "^6.0.2" - unique-filename "^1.1.1" - -cacache@^16.1.0: - version "16.1.3" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.3.tgz#a02b9f34ecfaf9a78c9f4bc16fceb94d5d67a38e" - integrity sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ== - dependencies: - "@npmcli/fs" "^2.1.0" - "@npmcli/move-file" "^2.0.0" - chownr "^2.0.0" - fs-minipass "^2.1.0" - glob "^8.0.1" - infer-owner "^1.0.4" - lru-cache "^7.7.1" - minipass "^3.1.6" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - mkdirp "^1.0.4" - p-map "^4.0.0" - promise-inflight "^1.0.1" - rimraf "^3.0.2" - ssri "^9.0.0" - tar "^6.1.11" - unique-filename "^2.0.0" - -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camelcase-keys@^6.2.2: - version "6.2.2" - resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz" - integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== - dependencies: - camelcase "^5.3.1" - map-obj "^4.0.0" - quick-lru "^4.0.1" - -camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -camelcase@^6.2.0: - version "6.3.0" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - -caniuse-lite@^1.0.30001400: - version "1.0.30001409" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001409.tgz" - integrity sha512-V0mnJ5dwarmhYv8/MzhJ//aW68UpvnQBXv8lJ2QUsvn2pHcmAuNtu8hQEDz37XnA1iE+lRR9CIfGWWpgJ5QedQ== - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" - integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== - -cbor-extract@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.1.1.tgz#f154b31529fdb6b7c70fb3ca448f44eda96a1b42" - integrity sha512-1UX977+L+zOJHsp0mWFG13GLwO6ucKgSmSW6JTl8B9GUvACvHeIVpFqhU92299Z6PfD09aTXDell5p+lp1rUFA== - dependencies: - node-gyp-build-optional-packages "5.0.3" - optionalDependencies: - "@cbor-extract/cbor-extract-darwin-arm64" "2.1.1" - "@cbor-extract/cbor-extract-darwin-x64" "2.1.1" - "@cbor-extract/cbor-extract-linux-arm" "2.1.1" - "@cbor-extract/cbor-extract-linux-arm64" "2.1.1" - "@cbor-extract/cbor-extract-linux-x64" "2.1.1" - "@cbor-extract/cbor-extract-win32-x64" "2.1.1" - -cbor-x@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.5.1.tgz#d2b0915c556c8ca294bebb4eac7d602218fd63c0" - integrity sha512-/vAkC4tiKCQCm5en4sA+mpKmjwY6Xxp1LO+BgZCNhp+Zow3pomyUHeBOK5EDp0mDaE36jw39l5eLHsoF3M1Lmg== - optionalDependencies: - cbor-extract "^2.1.1" - -cborg@^1.6.0: - version "1.9.5" - resolved "https://registry.npmjs.org/cborg/-/cborg-1.9.5.tgz" - integrity sha512-fLBv8wmqtlXqy1Yu+pHzevAIkW6k2K0ZtMujNzWphLsA34vzzg9BHn+5GmZqOJkSA9V7EMKsWrf6K976c1QMjQ== - -chalk@^2.0.0, chalk@^2.4.1: - version "2.4.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz" - integrity sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w== - -chalk@^5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/chalk/-/chalk-5.1.1.tgz" - integrity sha512-OItMegkSDU3P7OJRWBbNRsQsL8SzgwlIGXSZRVfHCLBYrDgzYDuozwDMwvEDpiZdjr50tdOTbTzuubirtEozsg== - -char-regex@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" - integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== - -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - -chownr@^1.1.1, chownr@^1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== - -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - -ci-info@^3.2.0: - version "3.4.0" - resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.4.0.tgz" - integrity sha512-t5QdPT5jq3o262DOQ8zA6E1tlH2upmUc4Hlvrbx1pGYJuiiHl7O7rvVNI+l8HTVhd/q3Qc9vqimkNk5yiXsAug== - -cjs-module-lexer@^1.0.0: - version "1.2.2" - resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz" - integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-width@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz" - integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== - -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" - integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== - -cluster-key-slot@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" - integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== - -cmd-shim@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/cmd-shim/-/cmd-shim-4.1.0.tgz" - integrity sha512-lb9L7EM4I/ZRVuljLPEtUJOP+xiQVknZ4ZMpMgEp4JzNldPb27HU03hi6K1/6CoIuit/Zm/LQXySErFeXxDprw== - dependencies: - mkdirp-infer-owner "^2.0.0" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" - integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== - -code-block-writer@^11.0.3: - version "11.0.3" - resolved "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz" - integrity sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw== - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz" - integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA== - -collect-v8-coverage@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz" - integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@^1.0.0, color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-string@^1.9.0: - version "1.9.1" - resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz" - integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color-support@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - -color@^4.2.3: - version "4.2.3" - resolved "https://registry.npmjs.org/color/-/color-4.2.3.tgz" - integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== - dependencies: - color-convert "^2.0.1" - color-string "^1.9.0" - -colorette@^2.0.7: - version "2.0.19" - resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz" - integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== - -columnify@^1.5.4: - version "1.6.0" - resolved "https://registry.npmjs.org/columnify/-/columnify-1.6.0.tgz" - integrity sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q== - dependencies: - strip-ansi "^6.0.1" - wcwidth "^1.0.0" - -combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@^9.4.0: - version "9.4.0" - resolved "https://registry.npmjs.org/commander/-/commander-9.4.0.tgz" - integrity sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw== - -common-tags@^1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" - integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== - -compare-func@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz" - integrity sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA== - dependencies: - array-ify "^1.0.0" - dot-prop "^5.1.0" - -compressible@~2.0.16: - version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" - debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" - vary "~1.1.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -concat-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz" - integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.0.2" - typedarray "^0.0.6" - -config-chain@^1.1.12: - version "1.1.13" - resolved "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz" - integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== - dependencies: - ini "^1.3.4" - proto-list "~1.2.1" - -console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" - integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== - -content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== - dependencies: - safe-buffer "5.2.1" - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -conventional-changelog-angular@^5.0.12: - version "5.0.13" - resolved "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz" - integrity sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA== - dependencies: - compare-func "^2.0.0" - q "^1.5.1" - -conventional-changelog-core@^4.2.2: - version "4.2.4" - resolved "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz" - integrity sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg== - dependencies: - add-stream "^1.0.0" - conventional-changelog-writer "^5.0.0" - conventional-commits-parser "^3.2.0" - dateformat "^3.0.0" - get-pkg-repo "^4.0.0" - git-raw-commits "^2.0.8" - git-remote-origin-url "^2.0.0" - git-semver-tags "^4.1.1" - lodash "^4.17.15" - normalize-package-data "^3.0.0" - q "^1.5.1" - read-pkg "^3.0.0" - read-pkg-up "^3.0.0" - through2 "^4.0.0" - -conventional-changelog-preset-loader@^2.3.4: - version "2.3.4" - resolved "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz" - integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== - -conventional-changelog-writer@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz" - integrity sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ== - dependencies: - conventional-commits-filter "^2.0.7" - dateformat "^3.0.0" - handlebars "^4.7.7" - json-stringify-safe "^5.0.1" - lodash "^4.17.15" - meow "^8.0.0" - semver "^6.0.0" - split "^1.0.0" - through2 "^4.0.0" - -conventional-commits-filter@^2.0.7: - version "2.0.7" - resolved "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz" - integrity sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA== - dependencies: - lodash.ismatch "^4.4.0" - modify-values "^1.0.0" - -conventional-commits-parser@^3.2.0: - version "3.2.4" - resolved "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz" - integrity sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q== - dependencies: - JSONStream "^1.0.4" - is-text-path "^1.0.1" - lodash "^4.17.15" - meow "^8.0.0" - split2 "^3.0.0" - through2 "^4.0.0" - -conventional-recommended-bump@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-6.1.0.tgz" - integrity sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw== - dependencies: - concat-stream "^2.0.0" - conventional-changelog-preset-loader "^2.3.4" - conventional-commits-filter "^2.0.7" - conventional-commits-parser "^3.2.0" - git-raw-commits "^2.0.8" - git-semver-tags "^4.1.1" - meow "^8.0.0" - q "^1.5.1" - -convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: - version "1.8.0" - resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz" - integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== - dependencies: - safe-buffer "~5.1.1" - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== - -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== - -core-js-compat@^3.25.1: - version "3.25.2" - resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.25.2.tgz" - integrity sha512-TxfyECD4smdn3/CjWxczVtJqVLEEC2up7/82t7vC0AzNogr+4nQ8vyF7abxAuTXWvjTClSbvGhU0RgqA4ToQaQ== - dependencies: - browserslist "^4.21.4" - -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" - integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -cors@^2.8.5: - version "2.8.5" - resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" - integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== - dependencies: - object-assign "^4" - vary "^1" - -cosmiconfig@^7.0.0: - version "7.0.1" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz" - integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.2.1" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.10.0" - -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - -cross-spawn@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -dargs@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz" - integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" - integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== - dependencies: - assert-plus "^1.0.0" - -dateformat@^3.0.0: - version "3.0.3" - resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz" - integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== - -dateformat@^4.6.3: - version "4.6.3" - resolved "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz" - integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== - -debug@2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - -debuglog@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz" - integrity sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw== - -decamelize-keys@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz" - integrity sha512-ocLWuYzRPoS9bfiSdDd3cxvrzovVMZnRDVEzAs+hWIVXGDbHxWMECij2OBuyB/An0FFW/nLuq6Kv1i/YC5Qfzg== - dependencies: - decamelize "^1.1.0" - map-obj "^1.0.0" - -decamelize@^1.1.0: - version "1.2.0" - resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" - integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz" - integrity sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og== - -decompress-response@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz" - integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== - dependencies: - mimic-response "^3.1.0" - -dedent@^0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz" - integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -deep-is@^0.1.3: - version "0.1.4" - resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - -deepmerge@^4.2.2: - version "4.2.2" - resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz" - integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== - -defaults@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz" - integrity sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA== - dependencies: - clone "^1.0.2" - -define-properties@^1.1.3, define-properties@^1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz" - integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== - dependencies: - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - -delay@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" - integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" - integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== - -denque@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" - integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== - -depd@2.0.0, depd@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -depd@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" - integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== - -deprecation@^2.0.0, deprecation@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz" - integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - -detect-indent@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz" - integrity sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g== - -detect-indent@^6.0.0: - version "6.1.0" - resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz" - integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== - -detect-libc@^2.0.0, detect-libc@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz" - integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== - -detect-newline@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" - integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== - -dezalgo@^1.0.0: - version "1.0.4" - resolved "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz" - integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== - dependencies: - asap "^2.0.0" - wrappy "1" - -diff-sequences@^28.1.1: - version "28.1.1" - resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz" - integrity sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw== - -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dom-serializer@^1.0.1: - version "1.4.1" - resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz" - integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.2.0" - entities "^2.0.0" - -domelementtype@^2.0.1, domelementtype@^2.2.0: - version "2.3.0" - resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz" - integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== - -domhandler@^4.0.0, domhandler@^4.2.0: - version "4.3.1" - resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz" - integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== - dependencies: - domelementtype "^2.2.0" - -domutils@^2.5.2: - version "2.8.0" - resolved "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz" - integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.2.0" - domhandler "^4.2.0" - -dot-prop@^5.1.0: - version "5.3.0" - resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz" - integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== - dependencies: - is-obj "^2.0.0" - -dot-prop@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz" - integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== - dependencies: - is-obj "^2.0.0" - -dotenv@^16.0.0, dotenv@^16.0.1, dotenv@^16.0.3: - version "16.0.3" - resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz" - integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== - -duplexer@^0.1.1: - version "0.1.2" - resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" - integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" - integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - -electron-to-chromium@^1.4.251: - version "1.4.257" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.257.tgz" - integrity sha512-C65sIwHqNnPC2ADMfse/jWTtmhZMII+x6ADI9gENzrOiI7BpxmfKFE84WkIEl5wEg+7+SfIkwChDlsd1Erju2A== - -elliptic@^6.4.1: - version "6.5.4" - resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz" - integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" - -emittery@^0.10.2: - version "0.10.2" - resolved "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz" - integrity sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -encoding@^0.1.12, encoding@^0.1.13: - version "0.1.13" - resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" - integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== - dependencies: - iconv-lite "^0.6.2" - -end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -env-paths@^2.2.0: - version "2.2.1" - resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz" - integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== - -envinfo@^7.7.4: - version "7.8.1" - resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz" - integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== - -err-code@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz" - integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.19.0, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.1: - version "1.20.2" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.2.tgz" - integrity sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ== - dependencies: - call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - function.prototype.name "^1.1.5" - get-intrinsic "^1.1.2" - get-symbol-description "^1.0.0" - has "^1.0.3" - has-property-descriptors "^1.0.0" - has-symbols "^1.0.3" - internal-slot "^1.0.3" - is-callable "^1.2.4" - is-negative-zero "^2.0.2" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - is-string "^1.0.7" - is-weakref "^1.0.2" - object-inspect "^1.12.2" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.4.3" - string.prototype.trimend "^1.0.5" - string.prototype.trimstart "^1.0.5" - unbox-primitive "^1.0.2" - -es-abstract@^1.20.4: - version "1.20.5" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.5.tgz#e6dc99177be37cacda5988e692c3fa8b218e95d2" - integrity sha512-7h8MM2EQhsCA7pU/Nv78qOXFpD8Rhqd12gYiSJVkrH9+e8VuA8JlPJK/hQjjlLv6pJvx/z1iRFKzYb0XT/RuAQ== - dependencies: - call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - function.prototype.name "^1.1.5" - get-intrinsic "^1.1.3" - get-symbol-description "^1.0.0" - gopd "^1.0.1" - has "^1.0.3" - has-property-descriptors "^1.0.0" - has-symbols "^1.0.3" - internal-slot "^1.0.3" - is-callable "^1.2.7" - is-negative-zero "^2.0.2" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - is-string "^1.0.7" - is-weakref "^1.0.2" - object-inspect "^1.12.2" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.4.3" - safe-regex-test "^1.0.0" - string.prototype.trimend "^1.0.6" - string.prototype.trimstart "^1.0.6" - unbox-primitive "^1.0.2" - -es-array-method-boxes-properly@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz" - integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -esbuild-android-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be" - integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ== - -esbuild-android-arm64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771" - integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg== - -esbuild-darwin-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25" - integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug== - -esbuild-darwin-arm64@0.14.54: - version "0.14.54" - resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz" - integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw== - -esbuild-freebsd-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d" - integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg== - -esbuild-freebsd-arm64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48" - integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q== - -esbuild-linux-32@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5" - integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw== - -esbuild-linux-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652" - integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg== - -esbuild-linux-arm64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b" - integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig== - -esbuild-linux-arm@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59" - integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw== - -esbuild-linux-mips64le@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34" - integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw== - -esbuild-linux-ppc64le@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e" - integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ== - -esbuild-linux-riscv64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8" - integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg== - -esbuild-linux-s390x@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6" - integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA== - -esbuild-netbsd-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81" - integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w== - -esbuild-node-externals@^1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/esbuild-node-externals/-/esbuild-node-externals-1.5.0.tgz" - integrity sha512-9394Ne2t2Z243BWeNBRkXEYVMOVbQuzp7XSkASZTOQs0GSXDuno5aH5OmzEXc6GMuln5zJjpkZpgwUPW0uRKgw== - dependencies: - find-up "5.0.0" - tslib "2.3.1" - -esbuild-openbsd-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b" - integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw== - -esbuild-plugin-copy@^1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/esbuild-plugin-copy/-/esbuild-plugin-copy-1.6.0.tgz" - integrity sha512-wN1paBCoE0yRBl9ZY3ZSD6SxGE4Yfr0Em7zh2yTbJv1JaHEIR3FYYN7HU6F+j/peSaGZJNSORSGxJ5QX1a1Sgg== - dependencies: - chalk "^4.1.2" - fs-extra "^10.0.1" - globby "^11.0.3" - -esbuild-sunos-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da" - integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw== - -esbuild-windows-32@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31" - integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w== - -esbuild-windows-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4" - integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ== - -esbuild-windows-arm64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982" - integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg== - -esbuild@^0.14.48: - version "0.14.54" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz" - integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA== - optionalDependencies: - "@esbuild/linux-loong64" "0.14.54" - esbuild-android-64 "0.14.54" - esbuild-android-arm64 "0.14.54" - esbuild-darwin-64 "0.14.54" - esbuild-darwin-arm64 "0.14.54" - esbuild-freebsd-64 "0.14.54" - esbuild-freebsd-arm64 "0.14.54" - esbuild-linux-32 "0.14.54" - esbuild-linux-64 "0.14.54" - esbuild-linux-arm "0.14.54" - esbuild-linux-arm64 "0.14.54" - esbuild-linux-mips64le "0.14.54" - esbuild-linux-ppc64le "0.14.54" - esbuild-linux-riscv64 "0.14.54" - esbuild-linux-s390x "0.14.54" - esbuild-netbsd-64 "0.14.54" - esbuild-openbsd-64 "0.14.54" - esbuild-sunos-64 "0.14.54" - esbuild-windows-32 "0.14.54" - esbuild-windows-64 "0.14.54" - esbuild-windows-arm64 "0.14.54" - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-config-prettier@^8.5.0: - version "8.5.0" - resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz" - integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== - -eslint-plugin-prettier@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b" - integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ== - dependencies: - prettier-linter-helpers "^1.0.0" - -eslint-scope@^5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -eslint-scope@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz" - integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^1.0.0: - version "1.3.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint-visitor-keys@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" - integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== - -eslint@^8.24.0: - version "8.24.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz" - integrity sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ== - dependencies: - "@eslint/eslintrc" "^1.3.2" - "@humanwhocodes/config-array" "^0.10.5" - "@humanwhocodes/gitignore-to-minimatch" "^1.0.2" - "@humanwhocodes/module-importer" "^1.0.1" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.3.0" - espree "^9.4.0" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.1" - globals "^13.15.0" - globby "^11.1.0" - grapheme-splitter "^1.0.4" - ignore "^5.2.0" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - js-sdsl "^4.1.4" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - -espree@^9.4.0: - version "9.4.0" - resolved "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz" - integrity sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw== - dependencies: - acorn "^8.8.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.3.0" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -eventemitter3@^4.0.4: - version "4.0.7" - resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - -events@3.3.0, events@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -execa@^5.0.0: - version "5.1.1" - resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - -exit@^0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" - integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== - -expand-template@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz" - integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== - -expect@^28.0.0, expect@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz" - integrity sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g== - dependencies: - "@jest/expect-utils" "^28.1.3" - jest-get-type "^28.0.2" - jest-matcher-utils "^28.1.3" - jest-message-util "^28.1.3" - jest-util "^28.1.3" - -express-async-errors@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/express-async-errors/-/express-async-errors-3.1.1.tgz" - integrity sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng== - -express@^4.17.2: - version "4.18.1" - resolved "https://registry.npmjs.org/express/-/express-4.18.1.tgz" - integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.0" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.5.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.2.0" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.7" - qs "6.10.3" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -express@^4.18.2: - version "4.18.2" - resolved "https://registry.npmjs.org/express/-/express-4.18.2.tgz" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.1" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.5.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.2.0" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.7" - qs "6.11.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" - integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== - -extsprintf@^1.2.0: - version "1.4.1" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz" - integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== - -fast-copy@^2.1.1: - version "2.1.7" - resolved "https://registry.npmjs.org/fast-copy/-/fast-copy-2.1.7.tgz" - integrity sha512-ozrGwyuCTAy7YgFCua8rmqmytECYk/JYAMXcswOcm0qvGoE3tPb7ivBeIHTOK2DiapBhDZgacIhzhQIKU5TCfA== - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-diff@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" - integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== - -fast-glob@^3.2.11, fast-glob@^3.2.9: - version "3.2.12" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" - integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-json-stringify@^2.7.10: - version "2.7.13" - resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-2.7.13.tgz#277aa86c2acba4d9851bd6108ed657aa327ed8c0" - integrity sha512-ar+hQ4+OIurUGjSJD1anvYSDcUflywhKjfxnsW4TBTD7+u0tJufv6DKRWoQk3vI6YBOWMoz0TQtfbe7dxbQmvA== - dependencies: - ajv "^6.11.0" - deepmerge "^4.2.2" - rfdc "^1.2.0" - string-similarity "^4.0.1" - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - -fast-printf@^1.6.9: - version "1.6.9" - resolved "https://registry.yarnpkg.com/fast-printf/-/fast-printf-1.6.9.tgz#212f56570d2dc8ccdd057ee93d50dd414d07d676" - integrity sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg== - dependencies: - boolean "^3.1.4" - -fast-redact@^3.1.1: - version "3.1.2" - resolved "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.2.tgz" - integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw== - -fast-safe-stringify@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz" - integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== - -fast-url-parser@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz" - integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ== - dependencies: - punycode "^1.3.2" - -fast-xml-parser@4.0.11: - version "4.0.11" - resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.0.11.tgz" - integrity sha512-4aUg3aNRR/WjQAcpceODG1C3x3lFANXRo8+1biqfieHmg9pyMt7qB4lQV/Ta6sJCTbA5vfD8fnA8S54JATiFUA== - dependencies: - strnum "^1.0.5" - -fastq@^1.6.0: - version "1.13.0" - resolved "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz" - integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== - dependencies: - reusify "^1.0.4" - -fb-watchman@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz" - integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== - dependencies: - bser "2.1.1" - -figures@^3.0.0: - version "3.2.0" - resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - -file-type@^16.5.4: - version "16.5.4" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-16.5.4.tgz#474fb4f704bee427681f98dd390058a172a6c2fd" - integrity sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw== - dependencies: - readable-web-to-node-stream "^3.0.0" - strtok3 "^6.2.4" - token-types "^4.1.1" - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -filter-obj@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz" - integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== - -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "2.4.1" - parseurl "~1.3.3" - statuses "2.0.1" - unpipe "~1.0.0" - -find-up@5.0.0, find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -find-up@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz" - integrity sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ== - dependencies: - locate-path "^2.0.0" - -find-up@^4.0.0, find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== - dependencies: - flatted "^3.1.0" - rimraf "^3.0.2" - -flatted@^3.1.0: - version "3.2.7" - resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz" - integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== - -follow-redirects@^1.14.9, follow-redirects@^1.15.0: - version "1.15.2" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" - integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== - -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== - -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -fs-extra@^10.0.1: - version "10.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz" - integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-extra@^9.1.0: - version "9.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-minipass@^1.2.7: - version "1.2.7" - resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz" - integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== - dependencies: - minipass "^2.6.0" - -fs-minipass@^2.0.0, fs-minipass@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== - dependencies: - minipass "^3.0.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@^2.3.2: - version "2.3.2" - resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -function.prototype.name@^1.1.5: - version "1.1.5" - resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz" - integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.0" - functions-have-names "^1.2.2" - -functions-have-names@^1.2.2: - version "1.2.3" - resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" - integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== - -gauge@^4.0.3: - version "4.0.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" - integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== - dependencies: - aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.3" - console-control-strings "^1.1.0" - has-unicode "^2.0.1" - signal-exit "^3.0.7" - string-width "^4.2.3" - strip-ansi "^6.0.1" - wide-align "^1.1.5" - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz" - integrity sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg== - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@^1.1.2, get-intrinsic@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz" - integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.3" - -get-package-type@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" - integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== - -get-pkg-repo@^4.0.0: - version "4.2.1" - resolved "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz" - integrity sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA== - dependencies: - "@hutson/parse-repository-url" "^3.0.0" - hosted-git-info "^4.0.0" - through2 "^2.0.0" - yargs "^16.2.0" - -get-port@^5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz" - integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== - -get-port@^6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/get-port/-/get-port-6.1.2.tgz#c1228abb67ba0e17fb346da33b15187833b9c08a" - integrity sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw== - -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - -get-symbol-description@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz" - integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz" - integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== - dependencies: - assert-plus "^1.0.0" - -git-raw-commits@^2.0.8: - version "2.0.11" - resolved "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz" - integrity sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A== - dependencies: - dargs "^7.0.0" - lodash "^4.17.15" - meow "^8.0.0" - split2 "^3.0.0" - through2 "^4.0.0" - -git-remote-origin-url@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz" - integrity sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw== - dependencies: - gitconfiglocal "^1.0.0" - pify "^2.3.0" - -git-semver-tags@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-4.1.1.tgz" - integrity sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA== - dependencies: - meow "^8.0.0" - semver "^6.0.0" - -git-up@^4.0.0: - version "4.0.5" - resolved "https://registry.npmjs.org/git-up/-/git-up-4.0.5.tgz" - integrity sha512-YUvVDg/vX3d0syBsk/CKUTib0srcQME0JyHkL5BaYdwLsiCslPWmDSi8PUMo9pXYjrryMcmsCoCgsTpSCJEQaA== - dependencies: - is-ssh "^1.3.0" - parse-url "^6.0.0" - -git-url-parse@^11.4.4: - version "11.6.0" - resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.6.0.tgz" - integrity sha512-WWUxvJs5HsyHL6L08wOusa/IXYtMuCAhrMmnTjQPpBU0TTHyDhnOATNH3xNQz7YOQUsqIIPTGr4xiVti1Hsk5g== - dependencies: - git-up "^4.0.0" - -gitconfiglocal@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz" - integrity sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ== - dependencies: - ini "^1.3.2" - -github-from-package@0.0.0: - version "0.0.0" - resolved "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz" - integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== - -glob-parent@^5.1.1, glob-parent@^5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.1: - version "6.0.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: - version "7.2.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^8.0.0: - version "8.0.3" - resolved "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz" - integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - -glob@^8.0.1: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globals@^13.15.0: - version "13.17.0" - resolved "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz" - integrity sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw== - dependencies: - type-fest "^0.20.2" - -globalthis@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" - integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== - dependencies: - define-properties "^1.1.3" - -globby@^11.0.2, globby@^11.0.3, globby@^11.1.0: - version "11.1.0" - resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.9: - version "4.2.10" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== - -graceful-fs@^4.2.6: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -grapheme-splitter@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== - -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== - -handlebars@^4.7.7: - version "4.7.7" - resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz" - integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== - dependencies: - minimist "^1.2.5" - neo-async "^2.6.0" - source-map "^0.6.1" - wordwrap "^1.0.0" - optionalDependencies: - uglify-js "^3.1.4" - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" - integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== - -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - -hard-rejection@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz" - integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== - -has-bigints@^1.0.1, has-bigints@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== - dependencies: - get-intrinsic "^1.1.1" - -has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== - dependencies: - has-symbols "^1.0.2" - -has-unicode@^2.0.0, has-unicode@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz" - integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -he@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -help-me@^4.0.1: - version "4.1.0" - resolved "https://registry.npmjs.org/help-me/-/help-me-4.1.0.tgz" - integrity sha512-5HMrkOks2j8Fpu2j5nTLhrBhT7VwHwELpqnSnx802ckofys5MO2SkLpgSz3dgNFHV7IYFX2igm5CM75SmuYidw== - dependencies: - glob "^8.0.0" - readable-stream "^3.6.0" - -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz" - integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -hosted-git-info@^2.1.4: - version "2.8.9" - resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== - -hosted-git-info@^4.0.0, hosted-git-info@^4.0.1: - version "4.1.0" - resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz" - integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== - dependencies: - lru-cache "^6.0.0" - -html-escaper@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" - integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== - -html-to-text@7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/html-to-text/-/html-to-text-7.1.1.tgz" - integrity sha512-c9QWysrfnRZevVpS8MlE7PyOdSuIOjg8Bt8ZE10jMU/BEngA6j3llj4GRfAmtQzcd1FjKE0sWu5IHXRUH9YxIQ== - dependencies: - deepmerge "^4.2.2" - he "^1.2.0" - htmlparser2 "^6.1.0" - minimist "^1.2.5" - -htmlparser2@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz" - integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.0.0" - domutils "^2.5.2" - entities "^2.0.0" - -http-cache-semantics@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== - -http-errors@2.0.0, http-errors@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -http-proxy-agent@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz" - integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== - dependencies: - "@tootallnate/once" "1" - agent-base "6" - debug "4" - -http-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" - integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== - dependencies: - "@tootallnate/once" "2" - agent-base "6" - debug "4" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz" - integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -http-terminator@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/http-terminator/-/http-terminator-3.2.0.tgz#bc158d2694b733ca4fbf22a35065a81a609fb3e9" - integrity sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g== - dependencies: - delay "^5.0.0" - p-wait-for "^3.2.0" - roarr "^7.0.4" - type-fest "^2.3.3" - -https-proxy-agent@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== - dependencies: - agent-base "6" - debug "4" - -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - -humanize-ms@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz" - integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== - dependencies: - ms "^2.0.0" - -iconv-lite@0.4.24, iconv-lite@^0.4.24: - version "0.4.24" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -iconv-lite@^0.6.2: - version "0.6.3" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore-walk@^3.0.3: - version "3.0.4" - resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz" - integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ== - dependencies: - minimatch "^3.0.4" - -ignore@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== - -import-fresh@^3.0.0, import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-local@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz" - integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== - dependencies: - pkg-dir "^4.2.0" - resolve-cwd "^3.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -infer-owner@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz" - integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ini@^1.3.2, ini@^1.3.4, ini@~1.3.0: - version "1.3.8" - resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -init-package-json@^2.0.2: - version "2.0.5" - resolved "https://registry.npmjs.org/init-package-json/-/init-package-json-2.0.5.tgz" - integrity sha512-u1uGAtEFu3VA6HNl/yUWw57jmKEMx8SKOxHhxjGnOFUiIlFnohKDFg4ZrPpv9wWqk44nDxGJAtqjdQFm+9XXQA== - dependencies: - npm-package-arg "^8.1.5" - promzard "^0.3.0" - read "~1.0.1" - read-package-json "^4.1.1" - semver "^7.3.5" - validate-npm-package-license "^3.0.4" - validate-npm-package-name "^3.0.0" - -inquirer@^7.3.3: - version "7.3.3" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz" - integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== - dependencies: - ansi-escapes "^4.2.1" - chalk "^4.1.0" - cli-cursor "^3.1.0" - cli-width "^3.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.19" - mute-stream "0.0.8" - run-async "^2.4.0" - rxjs "^6.6.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - through "^2.3.6" - -internal-slot@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz" - integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== - dependencies: - get-intrinsic "^1.1.0" - has "^1.0.3" - side-channel "^1.0.4" - -ioredis@^5.3.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.3.2.tgz#9139f596f62fc9c72d873353ac5395bcf05709f7" - integrity sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA== - dependencies: - "@ioredis/commands" "^1.1.1" - cluster-key-slot "^1.1.0" - debug "^4.3.4" - denque "^2.1.0" - lodash.defaults "^4.2.0" - lodash.isarguments "^3.1.0" - redis-errors "^1.2.0" - redis-parser "^3.0.0" - standard-as-callback "^2.1.0" - -ip@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz" - integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - -is-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== - -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== - dependencies: - has-bigints "^1.0.1" - -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-callable@^1.1.4, is-callable@^1.2.4: - version "1.2.6" - resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.6.tgz" - integrity sha512-krO72EO2NptOGAX2KYyqbP9vYMlNAXdB53rq6f8LXY6RY7JdSR/3BD6wLUlPHSAesmY9vstNrjvqGaCiRK/91Q== - -is-callable@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - -is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== - dependencies: - ci-info "^2.0.0" - -is-core-module@^2.5.0, is-core-module@^2.9.0: - version "2.10.0" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz" - integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== - dependencies: - has "^1.0.3" - -is-date-object@^1.0.1: - version "1.0.5" - resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz" - integrity sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw== - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-generator-fn@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz" - integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-lambda@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz" - integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== - -is-negative-zero@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz" - integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== - -is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== - dependencies: - has-tostringtag "^1.0.0" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz" - integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== - -is-plain-obj@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz" - integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== - -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-plain-object@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz" - integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== - -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-shared-array-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz" - integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== - dependencies: - call-bind "^1.0.2" - -is-ssh@^1.3.0: - version "1.4.0" - resolved "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz" - integrity sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ== - dependencies: - protocols "^2.0.1" - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== - dependencies: - has-tostringtag "^1.0.0" - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - -is-text-path@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz" - integrity sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w== - dependencies: - text-extensions "^1.0.0" - -is-typedarray@^1.0.0, is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" - integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== - -is-weakref@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== - dependencies: - call-bind "^1.0.2" - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -iso-datestring-validator@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz#2daa80d2900b7a954f9f731d42f96ee0c19a6895" - integrity sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA== - -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz" - integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" - integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== - -istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz" - integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== - -istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: - version "5.2.0" - resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz" - integrity sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A== - dependencies: - "@babel/core" "^7.12.3" - "@babel/parser" "^7.14.7" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.2.0" - semver "^6.3.0" - -istanbul-lib-report@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" - integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== - dependencies: - istanbul-lib-coverage "^3.0.0" - make-dir "^3.0.0" - supports-color "^7.1.0" - -istanbul-lib-source-maps@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz" - integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== - dependencies: - debug "^4.1.1" - istanbul-lib-coverage "^3.0.0" - source-map "^0.6.1" - -istanbul-reports@^3.1.3: - version "3.1.5" - resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz" - integrity sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w== - dependencies: - html-escaper "^2.0.0" - istanbul-lib-report "^3.0.0" - -jest-changed-files@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.1.3.tgz" - integrity sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA== - dependencies: - execa "^5.0.0" - p-limit "^3.1.0" - -jest-circus@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-circus/-/jest-circus-28.1.3.tgz" - integrity sha512-cZ+eS5zc79MBwt+IhQhiEp0OeBddpc1n8MBo1nMB8A7oPMKEO+Sre+wHaLJexQUj9Ya/8NOBY0RESUgYjB6fow== - dependencies: - "@jest/environment" "^28.1.3" - "@jest/expect" "^28.1.3" - "@jest/test-result" "^28.1.3" - "@jest/types" "^28.1.3" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - dedent "^0.7.0" - is-generator-fn "^2.0.0" - jest-each "^28.1.3" - jest-matcher-utils "^28.1.3" - jest-message-util "^28.1.3" - jest-runtime "^28.1.3" - jest-snapshot "^28.1.3" - jest-util "^28.1.3" - p-limit "^3.1.0" - pretty-format "^28.1.3" - slash "^3.0.0" - stack-utils "^2.0.3" - -jest-cli@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.3.tgz" - integrity sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ== - dependencies: - "@jest/core" "^28.1.3" - "@jest/test-result" "^28.1.3" - "@jest/types" "^28.1.3" - chalk "^4.0.0" - exit "^0.1.2" - graceful-fs "^4.2.9" - import-local "^3.0.2" - jest-config "^28.1.3" - jest-util "^28.1.3" - jest-validate "^28.1.3" - prompts "^2.0.1" - yargs "^17.3.1" - -jest-config@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-config/-/jest-config-28.1.3.tgz" - integrity sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ== - dependencies: - "@babel/core" "^7.11.6" - "@jest/test-sequencer" "^28.1.3" - "@jest/types" "^28.1.3" - babel-jest "^28.1.3" - chalk "^4.0.0" - ci-info "^3.2.0" - deepmerge "^4.2.2" - glob "^7.1.3" - graceful-fs "^4.2.9" - jest-circus "^28.1.3" - jest-environment-node "^28.1.3" - jest-get-type "^28.0.2" - jest-regex-util "^28.0.2" - jest-resolve "^28.1.3" - jest-runner "^28.1.3" - jest-util "^28.1.3" - jest-validate "^28.1.3" - micromatch "^4.0.4" - parse-json "^5.2.0" - pretty-format "^28.1.3" - slash "^3.0.0" - strip-json-comments "^3.1.1" - -jest-diff@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz" - integrity sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw== - dependencies: - chalk "^4.0.0" - diff-sequences "^28.1.1" - jest-get-type "^28.0.2" - pretty-format "^28.1.3" - -jest-docblock@^28.1.1: - version "28.1.1" - resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-28.1.1.tgz" - integrity sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA== - dependencies: - detect-newline "^3.0.0" - -jest-each@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-each/-/jest-each-28.1.3.tgz" - integrity sha512-arT1z4sg2yABU5uogObVPvSlSMQlDA48owx07BDPAiasW0yYpYHYOo4HHLz9q0BVzDVU4hILFjzJw0So9aCL/g== - dependencies: - "@jest/types" "^28.1.3" - chalk "^4.0.0" - jest-get-type "^28.0.2" - jest-util "^28.1.3" - pretty-format "^28.1.3" - -jest-environment-node@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.3.tgz" - integrity sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A== - dependencies: - "@jest/environment" "^28.1.3" - "@jest/fake-timers" "^28.1.3" - "@jest/types" "^28.1.3" - "@types/node" "*" - jest-mock "^28.1.3" - jest-util "^28.1.3" - -jest-get-type@^28.0.2: - version "28.0.2" - resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz" - integrity sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA== - -jest-haste-map@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.3.tgz" - integrity sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA== - dependencies: - "@jest/types" "^28.1.3" - "@types/graceful-fs" "^4.1.3" - "@types/node" "*" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.9" - jest-regex-util "^28.0.2" - jest-util "^28.1.3" - jest-worker "^28.1.3" - micromatch "^4.0.4" - walker "^1.0.8" - optionalDependencies: - fsevents "^2.3.2" - -jest-leak-detector@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.3.tgz" - integrity sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA== - dependencies: - jest-get-type "^28.0.2" - pretty-format "^28.1.3" - -jest-matcher-utils@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz" - integrity sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw== - dependencies: - chalk "^4.0.0" - jest-diff "^28.1.3" - jest-get-type "^28.0.2" - pretty-format "^28.1.3" - -jest-message-util@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz" - integrity sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g== - dependencies: - "@babel/code-frame" "^7.12.13" - "@jest/types" "^28.1.3" - "@types/stack-utils" "^2.0.0" - chalk "^4.0.0" - graceful-fs "^4.2.9" - micromatch "^4.0.4" - pretty-format "^28.1.3" - slash "^3.0.0" - stack-utils "^2.0.3" - -jest-mock@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz" - integrity sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA== - dependencies: - "@jest/types" "^28.1.3" - "@types/node" "*" - -jest-pnp-resolver@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz" - integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== - -jest-regex-util@^28.0.2: - version "28.0.2" - resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz" - integrity sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw== - -jest-resolve-dependencies@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.3.tgz" - integrity sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA== - dependencies: - jest-regex-util "^28.0.2" - jest-snapshot "^28.1.3" - -jest-resolve@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.3.tgz" - integrity sha512-Z1W3tTjE6QaNI90qo/BJpfnvpxtaFTFw5CDgwpyE/Kz8U/06N1Hjf4ia9quUhCh39qIGWF1ZuxFiBiJQwSEYKQ== - dependencies: - chalk "^4.0.0" - graceful-fs "^4.2.9" - jest-haste-map "^28.1.3" - jest-pnp-resolver "^1.2.2" - jest-util "^28.1.3" - jest-validate "^28.1.3" - resolve "^1.20.0" - resolve.exports "^1.1.0" - slash "^3.0.0" - -jest-runner@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.3.tgz" - integrity sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA== - dependencies: - "@jest/console" "^28.1.3" - "@jest/environment" "^28.1.3" - "@jest/test-result" "^28.1.3" - "@jest/transform" "^28.1.3" - "@jest/types" "^28.1.3" - "@types/node" "*" - chalk "^4.0.0" - emittery "^0.10.2" - graceful-fs "^4.2.9" - jest-docblock "^28.1.1" - jest-environment-node "^28.1.3" - jest-haste-map "^28.1.3" - jest-leak-detector "^28.1.3" - jest-message-util "^28.1.3" - jest-resolve "^28.1.3" - jest-runtime "^28.1.3" - jest-util "^28.1.3" - jest-watcher "^28.1.3" - jest-worker "^28.1.3" - p-limit "^3.1.0" - source-map-support "0.5.13" - -jest-runtime@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-28.1.3.tgz" - integrity sha512-NU+881ScBQQLc1JHG5eJGU7Ui3kLKrmwCPPtYsJtBykixrM2OhVQlpMmFWJjMyDfdkGgBMNjXCGB/ebzsgNGQw== - dependencies: - "@jest/environment" "^28.1.3" - "@jest/fake-timers" "^28.1.3" - "@jest/globals" "^28.1.3" - "@jest/source-map" "^28.1.2" - "@jest/test-result" "^28.1.3" - "@jest/transform" "^28.1.3" - "@jest/types" "^28.1.3" - chalk "^4.0.0" - cjs-module-lexer "^1.0.0" - collect-v8-coverage "^1.0.0" - execa "^5.0.0" - glob "^7.1.3" - graceful-fs "^4.2.9" - jest-haste-map "^28.1.3" - jest-message-util "^28.1.3" - jest-mock "^28.1.3" - jest-regex-util "^28.0.2" - jest-resolve "^28.1.3" - jest-snapshot "^28.1.3" - jest-util "^28.1.3" - slash "^3.0.0" - strip-bom "^4.0.0" - -jest-snapshot@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-28.1.3.tgz" - integrity sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg== - dependencies: - "@babel/core" "^7.11.6" - "@babel/generator" "^7.7.2" - "@babel/plugin-syntax-typescript" "^7.7.2" - "@babel/traverse" "^7.7.2" - "@babel/types" "^7.3.3" - "@jest/expect-utils" "^28.1.3" - "@jest/transform" "^28.1.3" - "@jest/types" "^28.1.3" - "@types/babel__traverse" "^7.0.6" - "@types/prettier" "^2.1.5" - babel-preset-current-node-syntax "^1.0.0" - chalk "^4.0.0" - expect "^28.1.3" - graceful-fs "^4.2.9" - jest-diff "^28.1.3" - jest-get-type "^28.0.2" - jest-haste-map "^28.1.3" - jest-matcher-utils "^28.1.3" - jest-message-util "^28.1.3" - jest-util "^28.1.3" - natural-compare "^1.4.0" - pretty-format "^28.1.3" - semver "^7.3.5" - -jest-util@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz" - integrity sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ== - dependencies: - "@jest/types" "^28.1.3" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.9" - picomatch "^2.2.3" - -jest-validate@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.3.tgz" - integrity sha512-SZbOGBWEsaTxBGCOpsRWlXlvNkvTkY0XxRfh7zYmvd8uL5Qzyg0CHAXiXKROflh801quA6+/DsT4ODDthOC/OA== - dependencies: - "@jest/types" "^28.1.3" - camelcase "^6.2.0" - chalk "^4.0.0" - jest-get-type "^28.0.2" - leven "^3.1.0" - pretty-format "^28.1.3" - -jest-watcher@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz" - integrity sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g== - dependencies: - "@jest/test-result" "^28.1.3" - "@jest/types" "^28.1.3" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - emittery "^0.10.2" - jest-util "^28.1.3" - string-length "^4.0.1" - -jest-worker@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz" - integrity sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jest@^28.1.2: - version "28.1.3" - resolved "https://registry.npmjs.org/jest/-/jest-28.1.3.tgz" - integrity sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA== - dependencies: - "@jest/core" "^28.1.3" - "@jest/types" "^28.1.3" - import-local "^3.0.2" - jest-cli "^28.1.3" - -joycon@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz" - integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== - -js-sdsl@^4.1.4: - version "4.1.4" - resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz" - integrity sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw== - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" - integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz" - integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== - -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-parse-even-better-errors@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz" - integrity sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema@0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz" - integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" - integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== - -json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" - integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== - -json5@^2.2.1: - version "2.2.1" - resolved "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz" - integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== - -jsonc-parser@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" - integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -jsonparse@^1.2.0, jsonparse@^1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" - integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== - -jsonwebtoken@^8.5.1: - version "8.5.1" - resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz" - integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^5.6.0" - -jsprim@^1.2.2: - version "1.4.2" - resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz" - integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" - -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - -key-encoder@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/key-encoder/-/key-encoder-2.0.3.tgz" - integrity sha512-fgBtpAGIr/Fy5/+ZLQZIPPhsZEcbSlYu/Wu96tNDFNSjSACw5lEIOFeaVdQ/iwrb8oxjlWi6wmWdH76hV6GZjg== - dependencies: - "@types/elliptic" "^6.4.9" - asn1.js "^5.0.1" - bn.js "^4.11.8" - elliptic "^6.4.1" - -kind-of@^6.0.2, kind-of@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -kleur@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== - -kysely@^0.22.0: - version "0.22.0" - resolved "https://registry.npmjs.org/kysely/-/kysely-0.22.0.tgz" - integrity sha512-ZE3qWtnqLOalodzfK5QUEcm7AEulhxsPNuKaGFsC3XiqO92vMLm+mAHk/NnbSIOtC4RmGm0nsv700i8KDp1gfQ== - -kysely@^0.23.4: - version "0.23.4" - resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.23.4.tgz#1975bfc37fb5074d60a415e8db73d5698528199a" - integrity sha512-3icLnj1fahUtZsP9zzOvF4DcdhekGsLX4ZaoBaIz0ZeHegyRDdbwpJD7zezAJ+KwQZNDeKchel6MikFNLsSZIA== - -lerna@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/lerna/-/lerna-4.0.0.tgz" - integrity sha512-DD/i1znurfOmNJb0OBw66NmNqiM8kF6uIrzrJ0wGE3VNdzeOhz9ziWLYiRaZDGGwgbcjOo6eIfcx9O5Qynz+kg== - dependencies: - "@lerna/add" "4.0.0" - "@lerna/bootstrap" "4.0.0" - "@lerna/changed" "4.0.0" - "@lerna/clean" "4.0.0" - "@lerna/cli" "4.0.0" - "@lerna/create" "4.0.0" - "@lerna/diff" "4.0.0" - "@lerna/exec" "4.0.0" - "@lerna/import" "4.0.0" - "@lerna/info" "4.0.0" - "@lerna/init" "4.0.0" - "@lerna/link" "4.0.0" - "@lerna/list" "4.0.0" - "@lerna/publish" "4.0.0" - "@lerna/run" "4.0.0" - "@lerna/version" "4.0.0" - import-local "^3.0.2" - npmlog "^4.1.2" - -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -libnpmaccess@^4.0.1: - version "4.0.3" - resolved "https://registry.npmjs.org/libnpmaccess/-/libnpmaccess-4.0.3.tgz" - integrity sha512-sPeTSNImksm8O2b6/pf3ikv4N567ERYEpeKRPSmqlNt1dTZbvgpJIzg5vAhXHpw2ISBsELFRelk0jEahj1c6nQ== - dependencies: - aproba "^2.0.0" - minipass "^3.1.1" - npm-package-arg "^8.1.2" - npm-registry-fetch "^11.0.0" - -libnpmpublish@^4.0.0: - version "4.0.2" - resolved "https://registry.npmjs.org/libnpmpublish/-/libnpmpublish-4.0.2.tgz" - integrity sha512-+AD7A2zbVeGRCFI2aO//oUmapCwy7GHqPXFJh3qpToSRNU+tXKJ2YFUgjt04LPPAf2dlEH95s6EhIHM1J7bmOw== - dependencies: - normalize-package-data "^3.0.2" - npm-package-arg "^8.1.2" - npm-registry-fetch "^11.0.0" - semver "^7.1.3" - ssri "^8.0.1" - -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz" - integrity sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw== - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - -load-json-file@^6.2.0: - version "6.2.0" - resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz" - integrity sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ== - dependencies: - graceful-fs "^4.1.15" - parse-json "^5.0.0" - strip-bom "^4.0.0" - type-fest "^0.6.0" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz" - integrity sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA== - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -lodash._reinterpolate@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz" - integrity sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA== - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" - integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== - -lodash.defaults@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" - integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== - -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz" - integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== - -lodash.isarguments@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz" - integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz" - integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== - -lodash.ismatch@^4.4.0: - version "4.4.0" - resolved "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz" - integrity sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g== - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz" - integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz" - integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz" - integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz" - integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== - -lodash.template@^4.5.0: - version "4.5.0" - resolved "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz" - integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== - dependencies: - lodash._reinterpolate "^3.0.0" - lodash.templatesettings "^4.0.0" - -lodash.templatesettings@^4.0.0: - version "4.2.0" - resolved "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz" - integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== - dependencies: - lodash._reinterpolate "^3.0.0" - -lodash@^4.17.15, lodash@^4.17.19, lodash@^4.7.0: - version "4.17.21" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -lru-cache@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a" - integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g== - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -lru-cache@^7.7.1: - version "7.18.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" - integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== - -make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - -make-dir@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -make-fetch-happen@^10.0.3: - version "10.2.1" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz#f5e3835c5e9817b617f2770870d9492d28678164" - integrity sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w== - dependencies: - agentkeepalive "^4.2.1" - cacache "^16.1.0" - http-cache-semantics "^4.1.0" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" - is-lambda "^1.0.1" - lru-cache "^7.7.1" - minipass "^3.1.6" - minipass-collect "^1.0.2" - minipass-fetch "^2.0.3" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - negotiator "^0.6.3" - promise-retry "^2.0.1" - socks-proxy-agent "^7.0.0" - ssri "^9.0.0" - -make-fetch-happen@^8.0.9: - version "8.0.14" - resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz" - integrity sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ== - dependencies: - agentkeepalive "^4.1.3" - cacache "^15.0.5" - http-cache-semantics "^4.1.0" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - is-lambda "^1.0.1" - lru-cache "^6.0.0" - minipass "^3.1.3" - minipass-collect "^1.0.2" - minipass-fetch "^1.3.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - promise-retry "^2.0.1" - socks-proxy-agent "^5.0.0" - ssri "^8.0.0" - -make-fetch-happen@^9.0.1: - version "9.1.0" - resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz" - integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== - dependencies: - agentkeepalive "^4.1.3" - cacache "^15.2.0" - http-cache-semantics "^4.1.0" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - is-lambda "^1.0.1" - lru-cache "^6.0.0" - minipass "^3.1.3" - minipass-collect "^1.0.2" - minipass-fetch "^1.3.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - negotiator "^0.6.2" - promise-retry "^2.0.1" - socks-proxy-agent "^6.0.0" - ssri "^8.0.0" - -makeerror@1.0.12: - version "1.0.12" - resolved "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz" - integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== - dependencies: - tmpl "1.0.5" - -map-obj@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz" - integrity sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg== - -map-obj@^4.0.0: - version "4.3.0" - resolved "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz" - integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" - integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== - -memorystream@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" - integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== - -meow@^8.0.0: - version "8.1.2" - resolved "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz" - integrity sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q== - dependencies: - "@types/minimist" "^1.2.0" - camelcase-keys "^6.2.2" - decamelize-keys "^1.1.0" - hard-rejection "^2.1.0" - minimist-options "4.1.0" - normalize-package-data "^3.0.0" - read-pkg-up "^7.0.1" - redent "^3.0.0" - trim-newlines "^3.0.0" - type-fest "^0.18.0" - yargs-parser "^20.2.3" - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== - -micromatch@^4.0.4: - version "4.0.5" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== - dependencies: - braces "^3.0.2" - picomatch "^2.3.1" - -mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": - version "1.52.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12, mime-types@^2.1.35, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -mimic-response@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz" - integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== - -min-indent@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" - integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== - -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz" - integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== - -minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^5.0.1, minimatch@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz" - integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== - dependencies: - brace-expansion "^2.0.1" - -minimist-options@4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz" - integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== - dependencies: - arrify "^1.0.1" - is-plain-obj "^1.1.0" - kind-of "^6.0.3" - -minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: - version "1.2.6" - resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== - -minipass-collect@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz" - integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== - dependencies: - minipass "^3.0.0" - -minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: - version "1.4.1" - resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz" - integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== - dependencies: - minipass "^3.1.0" - minipass-sized "^1.0.3" - minizlib "^2.0.0" - optionalDependencies: - encoding "^0.1.12" - -minipass-fetch@^2.0.3: - version "2.1.2" - resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-2.1.2.tgz#95560b50c472d81a3bc76f20ede80eaed76d8add" - integrity sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA== - dependencies: - minipass "^3.1.6" - minipass-sized "^1.0.3" - minizlib "^2.1.2" - optionalDependencies: - encoding "^0.1.13" - -minipass-flush@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz" - integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== - dependencies: - minipass "^3.0.0" - -minipass-json-stream@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz" - integrity sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg== - dependencies: - jsonparse "^1.3.1" - minipass "^3.0.0" - -minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz" - integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== - dependencies: - minipass "^3.0.0" - -minipass-sized@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz" - integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== - dependencies: - minipass "^3.0.0" - -minipass@^2.6.0, minipass@^2.9.0: - version "2.9.0" - resolved "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz" - integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== - dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" - -minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: - version "3.3.4" - resolved "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz" - integrity sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw== - dependencies: - yallist "^4.0.0" - -minipass@^3.1.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" - integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== - dependencies: - yallist "^4.0.0" - -minipass@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" - integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== - -minizlib@^1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz" - integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== - dependencies: - minipass "^2.9.0" - -minizlib@^2.0.0, minizlib@^2.1.1, minizlib@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== - dependencies: - minipass "^3.0.0" - yallist "^4.0.0" - -mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: - version "0.5.3" - resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz" - integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== - -mkdirp-infer-owner@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/mkdirp-infer-owner/-/mkdirp-infer-owner-2.0.0.tgz" - integrity sha512-sdqtiFt3lkOaYvTXSRIUjkIdPTcxgv5+fgqYE/5qgwdw12cOrAuzzgzvVExIkH/ul1oeHN3bCLOWSG3XOqbKKw== - dependencies: - chownr "^2.0.0" - infer-owner "^1.0.4" - mkdirp "^1.0.3" - -mkdirp@^0.5.1, mkdirp@^0.5.5: - version "0.5.6" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" - integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== - dependencies: - minimist "^1.2.6" - -mkdirp@^1.0.3, mkdirp@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -modify-values@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz" - integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@2.1.3, ms@^2.0.0, ms@^2.1.1: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -multiformats@^9.4.2, multiformats@^9.5.4, multiformats@^9.6.4: - version "9.9.0" - resolved "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz" - integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg== - -multimatch@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz" - integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA== - dependencies: - "@types/minimatch" "^3.0.3" - array-differ "^3.0.0" - array-union "^2.1.0" - arrify "^2.0.1" - minimatch "^3.0.4" - -mute-stream@0.0.8, mute-stream@~0.0.4: - version "0.0.8" - resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" - integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== - -napi-build-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz" - integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" - integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== - -negotiator@0.6.3, negotiator@^0.6.2, negotiator@^0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - -neo-async@^2.6.0: - version "2.6.2" - resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -node-abi@^3.3.0: - version "3.26.0" - resolved "https://registry.npmjs.org/node-abi/-/node-abi-3.26.0.tgz" - integrity sha512-jRVtMFTChbi2i/jqo/i2iP9634KMe+7K1v35mIdj3Mn59i5q27ZYhn+sW6npISM/PQg7HrP2kwtRBMmh5Uvzdg== - dependencies: - semver "^7.3.5" - -node-addon-api@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz" - integrity sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA== - -node-fetch@^2.6.1, node-fetch@^2.6.7: - version "2.6.7" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - -node-gyp-build-optional-packages@5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17" - integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA== - -node-gyp@^5.0.2: - version "5.1.1" - resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.1.tgz" - integrity sha512-WH0WKGi+a4i4DUt2mHnvocex/xPLp9pYt5R6M2JdFB7pJ7Z34hveZ4nDTGTiLXCkitA9T8HFZjhinBCiVHYcWw== - dependencies: - env-paths "^2.2.0" - glob "^7.1.4" - graceful-fs "^4.2.2" - mkdirp "^0.5.1" - nopt "^4.0.1" - npmlog "^4.1.2" - request "^2.88.0" - rimraf "^2.6.3" - semver "^5.7.1" - tar "^4.4.12" - which "^1.3.1" - -node-gyp@^7.1.0: - version "7.1.2" - resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz" - integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ== - dependencies: - env-paths "^2.2.0" - glob "^7.1.4" - graceful-fs "^4.2.3" - nopt "^5.0.0" - npmlog "^4.1.2" - request "^2.88.2" - rimraf "^3.0.2" - semver "^7.3.2" - tar "^6.0.2" - which "^2.0.2" - -node-gyp@^9.3.1: - version "9.3.1" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.3.1.tgz#1e19f5f290afcc9c46973d68700cbd21a96192e4" - integrity sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg== - dependencies: - env-paths "^2.2.0" - glob "^7.1.4" - graceful-fs "^4.2.6" - make-fetch-happen "^10.0.3" - nopt "^6.0.0" - npmlog "^6.0.0" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.2" - which "^2.0.2" - -node-int64@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" - integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== - -node-releases@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz" - integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== - -nodemailer-html-to-text@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/nodemailer-html-to-text/-/nodemailer-html-to-text-3.2.0.tgz" - integrity sha512-RJUC6640QV1PzTHHapOrc6IzrAJUZtk2BdVdINZ9VTLm+mcQNyBO9LYyhrnufkzqiD9l8hPLJ97rSyK4WanPNg== - dependencies: - html-to-text "7.1.1" - -nodemailer@^6.8.0: - version "6.8.0" - resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.8.0.tgz" - integrity sha512-EjYvSmHzekz6VNkNd12aUqAco+bOkRe3Of5jVhltqKhEsjw/y0PYPJfp83+s9Wzh1dspYAkUW/YNQ350NATbSQ== - -nopt@^4.0.1: - version "4.0.3" - resolved "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz" - integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== - dependencies: - abbrev "1" - osenv "^0.1.4" - -nopt@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz" - integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== - dependencies: - abbrev "1" - -nopt@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" - integrity sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g== - dependencies: - abbrev "^1.0.0" - -normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-package-data@^3.0.0, normalize-package-data@^3.0.2: - version "3.0.3" - resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz" - integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== - dependencies: - hosted-git-info "^4.0.1" - is-core-module "^2.5.0" - semver "^7.3.4" - validate-npm-package-license "^3.0.1" - -normalize-path@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-url@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz" - integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== - -npm-bundled@^1.1.1: - version "1.1.2" - resolved "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz" - integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== - dependencies: - npm-normalize-package-bin "^1.0.1" - -npm-install-checks@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz" - integrity sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w== - dependencies: - semver "^7.1.1" - -npm-lifecycle@^3.1.5: - version "3.1.5" - resolved "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-3.1.5.tgz" - integrity sha512-lDLVkjfZmvmfvpvBzA4vzee9cn+Me4orq0QF8glbswJVEbIcSNWib7qGOffolysc3teCqbbPZZkzbr3GQZTL1g== - dependencies: - byline "^5.0.0" - graceful-fs "^4.1.15" - node-gyp "^5.0.2" - resolve-from "^4.0.0" - slide "^1.1.6" - uid-number "0.0.6" - umask "^1.1.0" - which "^1.3.1" - -npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz" - integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== - -npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.0, npm-package-arg@^8.1.2, npm-package-arg@^8.1.5: - version "8.1.5" - resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.5.tgz" - integrity sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q== - dependencies: - hosted-git-info "^4.0.1" - semver "^7.3.4" - validate-npm-package-name "^3.0.0" - -npm-packlist@^2.1.4: - version "2.2.2" - resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-2.2.2.tgz" - integrity sha512-Jt01acDvJRhJGthnUJVF/w6gumWOZxO7IkpY/lsX9//zqQgnF7OJaxgQXcerd4uQOLu7W5bkb4mChL9mdfm+Zg== - dependencies: - glob "^7.1.6" - ignore-walk "^3.0.3" - npm-bundled "^1.1.1" - npm-normalize-package-bin "^1.0.1" - -npm-pick-manifest@^6.0.0, npm-pick-manifest@^6.1.1: - version "6.1.1" - resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz" - integrity sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA== - dependencies: - npm-install-checks "^4.0.0" - npm-normalize-package-bin "^1.0.1" - npm-package-arg "^8.1.2" - semver "^7.3.4" - -npm-registry-fetch@^11.0.0: - version "11.0.0" - resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-11.0.0.tgz" - integrity sha512-jmlgSxoDNuhAtxUIG6pVwwtz840i994dL14FoNVZisrmZW5kWd63IUTNv1m/hyRSGSqWjCUp/YZlS1BJyNp9XA== - dependencies: - make-fetch-happen "^9.0.1" - minipass "^3.1.3" - minipass-fetch "^1.3.0" - minipass-json-stream "^1.0.1" - minizlib "^2.0.0" - npm-package-arg "^8.0.0" - -npm-registry-fetch@^9.0.0: - version "9.0.0" - resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-9.0.0.tgz" - integrity sha512-PuFYYtnQ8IyVl6ib9d3PepeehcUeHN9IO5N/iCRhyg9tStQcqGQBRVHmfmMWPDERU3KwZoHFvbJ4FPXPspvzbA== - dependencies: - "@npmcli/ci-detect" "^1.0.0" - lru-cache "^6.0.0" - make-fetch-happen "^8.0.9" - minipass "^3.1.3" - minipass-fetch "^1.3.0" - minipass-json-stream "^1.0.1" - minizlib "^2.0.0" - npm-package-arg "^8.0.0" - -npm-run-all@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba" - integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ== - dependencies: - ansi-styles "^3.2.1" - chalk "^2.4.1" - cross-spawn "^6.0.5" - memorystream "^0.3.1" - minimatch "^3.0.4" - pidtree "^0.3.0" - read-pkg "^3.0.0" - shell-quote "^1.6.1" - string.prototype.padend "^3.0.0" - -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -npmlog@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -npmlog@^6.0.0: - version "6.0.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" - integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== - dependencies: - are-we-there-yet "^3.0.0" - console-control-strings "^1.1.0" - gauge "^4.0.3" - set-blocking "^2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" - integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ== - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-assign@^4, object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-inspect@^1.12.2, object-inspect@^1.9.0: - version "1.12.2" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz" - integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.0, object.assign@^4.1.4: - version "4.1.4" - resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz" - integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - has-symbols "^1.0.3" - object-keys "^1.1.1" - -object.getownpropertydescriptors@^2.0.3: - version "2.1.4" - resolved "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.4.tgz" - integrity sha512-sccv3L/pMModT6dJAYF3fzGMVcb38ysQ0tEE6ixv2yXJDtEIPph268OlAdJj5/qZMZDq2g/jqvwppt36uS/uQQ== - dependencies: - array.prototype.reduce "^1.0.4" - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.1" - -on-exit-leak-free@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz" - integrity sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w== - -on-finished@2.4.1: - version "2.4.1" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -one-webcrypto@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/one-webcrypto/-/one-webcrypto-1.0.3.tgz" - integrity sha512-fu9ywBVBPx0gS9K0etIROTiCkvI5S1TDjFsYFb3rC1ewFxeOqsbzq7aIMBHsYfrTHBcGXJaONXXjTl8B01cW1Q== - -onetime@^5.1.0, onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== - dependencies: - deep-is "^0.1.3" - fast-levenshtein "^2.0.6" - levn "^0.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - word-wrap "^1.2.3" - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz" - integrity sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ== - -os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" - integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== - -osenv@^0.1.4: - version "0.1.5" - resolved "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz" - integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz" - integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2, p-limit@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz" - integrity sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg== - dependencies: - p-limit "^1.1.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -p-map-series@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/p-map-series/-/p-map-series-2.1.0.tgz" - integrity sha512-RpYIIK1zXSNEOdwxcfe7FdvGcs7+y5n8rifMhMNWvaxRNMPINJHF5GDeuVxWqnfrcHPSCnp7Oo5yNXHId9Av2Q== - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -p-pipe@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/p-pipe/-/p-pipe-3.1.0.tgz" - integrity sha512-08pj8ATpzMR0Y80x50yJHn37NF6vjrqHutASaX5LiH5npS9XPvrUmscd9MF5R4fuYRHOxQR1FfMIlF7AzwoPqw== - -p-queue@^6.6.2: - version "6.6.2" - resolved "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz" - integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== - dependencies: - eventemitter3 "^4.0.4" - p-timeout "^3.2.0" - -p-reduce@^2.0.0, p-reduce@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz" - integrity sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw== - -p-timeout@^3.0.0, p-timeout@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz" - integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== - dependencies: - p-finally "^1.0.0" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz" - integrity sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww== - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -p-wait-for@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/p-wait-for/-/p-wait-for-3.2.0.tgz#640429bcabf3b0dd9f492c31539c5718cb6a3f1f" - integrity sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA== - dependencies: - p-timeout "^3.0.0" - -p-waterfall@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/p-waterfall/-/p-waterfall-2.1.1.tgz" - integrity sha512-RRTnDb2TBG/epPRI2yYXsimO0v3BXC8Yd3ogr1545IaqKK17VGhbWVeGGN+XfCm/08OK8635nH31c8bATkHuSw== - dependencies: - p-reduce "^2.0.0" - -packet-reader@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz" - integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== - -pacote@^11.2.6: - version "11.3.5" - resolved "https://registry.npmjs.org/pacote/-/pacote-11.3.5.tgz" - integrity sha512-fT375Yczn4zi+6Hkk2TBe1x1sP8FgFsEIZ2/iWaXY2r/NkhDJfxbcn5paz1+RTFCyNf+dPnaoBDJoAxXSU8Bkg== - dependencies: - "@npmcli/git" "^2.1.0" - "@npmcli/installed-package-contents" "^1.0.6" - "@npmcli/promise-spawn" "^1.2.0" - "@npmcli/run-script" "^1.8.2" - cacache "^15.0.5" - chownr "^2.0.0" - fs-minipass "^2.1.0" - infer-owner "^1.0.4" - minipass "^3.1.3" - mkdirp "^1.0.3" - npm-package-arg "^8.0.1" - npm-packlist "^2.1.4" - npm-pick-manifest "^6.0.0" - npm-registry-fetch "^11.0.0" - promise-retry "^2.0.1" - read-package-json-fast "^2.0.1" - rimraf "^3.0.2" - ssri "^8.0.1" - tar "^6.1.0" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz" - integrity sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw== - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -parse-json@^5.0.0, parse-json@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - -parse-path@^4.0.0: - version "4.0.4" - resolved "https://registry.npmjs.org/parse-path/-/parse-path-4.0.4.tgz" - integrity sha512-Z2lWUis7jlmXC1jeOG9giRO2+FsuyNipeQ43HAjqAZjwSe3SEf+q/84FGPHoso3kyntbxa4c4i77t3m6fGf8cw== - dependencies: - is-ssh "^1.3.0" - protocols "^1.4.0" - qs "^6.9.4" - query-string "^6.13.8" - -parse-url@^6.0.0: - version "6.0.5" - resolved "https://registry.npmjs.org/parse-url/-/parse-url-6.0.5.tgz" - integrity sha512-e35AeLTSIlkw/5GFq70IN7po8fmDUjpDPY1rIK+VubRfsUvBonjQ+PBZG+vWMACnQSmNlvl524IucoDmcioMxA== - dependencies: - is-ssh "^1.3.0" - normalize-url "^6.1.0" - parse-path "^4.0.0" - protocols "^1.4.0" - -parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -path-browserify@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz" - integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz" - integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== - -path-type@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz" - integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== - dependencies: - pify "^3.0.0" - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -peek-readable@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72" - integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg== - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" - integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== - -pg-connection-string@^2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz" - integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== - -pg-int8@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz" - integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== - -pg-pool@^3.5.2: - version "3.5.2" - resolved "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz" - integrity sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w== - -pg-pool@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.0.tgz#3190df3e4747a0d23e5e9e8045bcd99bda0a712e" - integrity sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ== - -pg-protocol@*, pg-protocol@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.0.tgz#4c91613c0315349363af2084608db843502f8833" - integrity sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q== - -pg-types@^2.1.0, pg-types@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz" - integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== - dependencies: - pg-int8 "1.0.1" - postgres-array "~2.0.0" - postgres-bytea "~1.0.0" - postgres-date "~1.0.4" - postgres-interval "^1.1.0" - -pg@^8.10.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.10.0.tgz#5b8379c9b4a36451d110fc8cd98fc325fe62ad24" - integrity sha512-ke7o7qSTMb47iwzOSaZMfeR7xToFdkE71ifIipOAAaLIM0DYzfOAXlgFFmYUIE2BcJtvnVlGCID84ZzCegE8CQ== - dependencies: - buffer-writer "2.0.0" - packet-reader "1.0.0" - pg-connection-string "^2.5.0" - pg-pool "^3.6.0" - pg-protocol "^1.6.0" - pg-types "^2.1.0" - pgpass "1.x" - -pg@^8.9.0: - version "8.9.0" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.9.0.tgz#73c5d77a854d36b0e185450dacb8b90c669e040b" - integrity sha512-ZJM+qkEbtOHRuXjmvBtOgNOXOtLSbxiMiUVMgE4rV6Zwocy03RicCVvDXgx8l4Biwo8/qORUnEqn2fdQzV7KCg== - dependencies: - buffer-writer "2.0.0" - packet-reader "1.0.0" - pg-connection-string "^2.5.0" - pg-pool "^3.5.2" - pg-protocol "^1.6.0" - pg-types "^2.1.0" - pgpass "1.x" - -pgpass@1.x: - version "1.0.5" - resolved "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz" - integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== - dependencies: - split2 "^4.1.0" - -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -pidtree@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a" - integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA== - -pify@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" - integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz" - integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pify@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz" - integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== - -pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz" - integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA== - dependencies: - readable-stream "^4.0.0" - split2 "^4.0.0" - -pino-http@^8.2.1: - version "8.2.1" - resolved "https://registry.npmjs.org/pino-http/-/pino-http-8.2.1.tgz" - integrity sha512-bdWAE4HYfFjDhKw2/N7BLNSIFAs+WDLZnetsGRpBdNEKq7/RoZUgblLS5OlMY257RPQml6J5QiiLkwxbstzWbA== - dependencies: - fast-url-parser "^1.1.3" - get-caller-file "^2.0.5" - pino "^8.0.0" - pino-std-serializers "^6.0.0" - process-warning "^2.0.0" - -pino-http@^8.3.3: - version "8.3.3" - resolved "https://registry.yarnpkg.com/pino-http/-/pino-http-8.3.3.tgz#2b140e734bfc6babe0df272a43bb8f36f2b525c0" - integrity sha512-p4umsNIXXVu95HD2C8wie/vXH7db5iGRpc+yj1/ZQ3sRtTQLXNjoS6Be5+eI+rQbqCRxen/7k/KSN+qiZubGDw== - dependencies: - get-caller-file "^2.0.5" - pino "^8.0.0" - pino-std-serializers "^6.0.0" - process-warning "^2.0.0" - -pino-pretty@^9.1.0: - version "9.1.0" - resolved "https://registry.npmjs.org/pino-pretty/-/pino-pretty-9.1.0.tgz" - integrity sha512-IM6NY9LLo/dVgY7/prJhCh4rAJukafdt0ibxeNOWc2fxKMyTk90SOB9Ao2HfbtShT9QPeP0ePpJktksMhSQMYA== - dependencies: - colorette "^2.0.7" - dateformat "^4.6.3" - fast-copy "^2.1.1" - fast-safe-stringify "^2.1.1" - help-me "^4.0.1" - joycon "^3.1.1" - minimist "^1.2.6" - on-exit-leak-free "^2.1.0" - pino-abstract-transport "^1.0.0" - pump "^3.0.0" - readable-stream "^4.0.0" - secure-json-parse "^2.4.0" - sonic-boom "^3.0.0" - strip-json-comments "^3.1.1" - -pino-std-serializers@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.0.0.tgz" - integrity sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ== - -pino@^8.0.0, pino@^8.6.1: - version "8.6.1" - resolved "https://registry.npmjs.org/pino/-/pino-8.6.1.tgz" - integrity sha512-fi+V2K98eMZjQ/uEHHSiMALNrz7HaFdKNYuyA3ZUrbH0f1e8sPFDmeRGzg7ZH2q4QDxGnJPOswmqlEaTAZeDPA== - dependencies: - atomic-sleep "^1.0.0" - fast-redact "^3.1.1" - on-exit-leak-free "^2.1.0" - pino-abstract-transport v1.0.0 - pino-std-serializers "^6.0.0" - process-warning "^2.0.0" - quick-format-unescaped "^4.0.3" - real-require "^0.2.0" - safe-stable-stringify "^2.3.1" - sonic-boom "^3.1.0" - thread-stream "^2.0.0" - -pino@^8.11.0: - version "8.11.0" - resolved "https://registry.yarnpkg.com/pino/-/pino-8.11.0.tgz#2a91f454106b13e708a66c74ebc1c2ab7ab38498" - integrity sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg== - dependencies: - atomic-sleep "^1.0.0" - fast-redact "^3.1.1" - on-exit-leak-free "^2.1.0" - pino-abstract-transport v1.0.0 - pino-std-serializers "^6.0.0" - process-warning "^2.0.0" - quick-format-unescaped "^4.0.3" - real-require "^0.2.0" - safe-stable-stringify "^2.3.1" - sonic-boom "^3.1.0" - thread-stream "^2.0.0" - -pirates@^4.0.4: - version "4.0.5" - resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz" - integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== - -pkg-dir@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - -postgres-array@~2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz" - integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== - -postgres-bytea@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz" - integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== - -postgres-date@~1.0.4: - version "1.0.7" - resolved "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz" - integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== - -postgres-interval@^1.1.0: - version "1.2.0" - resolved "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz" - integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== - dependencies: - xtend "^4.0.0" - -prebuild-install@^7.1.0, prebuild-install@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz" - integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== - dependencies: - detect-libc "^2.0.0" - expand-template "^2.0.3" - github-from-package "0.0.0" - minimist "^1.2.3" - mkdirp-classic "^0.5.3" - napi-build-utils "^1.0.1" - node-abi "^3.3.0" - pump "^3.0.0" - rc "^1.2.7" - simple-get "^4.0.0" - tar-fs "^2.0.0" - tunnel-agent "^0.6.0" - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -prettier-config-standard@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/prettier-config-standard/-/prettier-config-standard-5.0.0.tgz" - integrity sha512-QK252QwCxlsak8Zx+rPKZU31UdbRcu9iUk9X1ONYtLSO221OgvV9TlKoTf6iPDZtvF3vE2mkgzFIEgSUcGELSQ== - -prettier-linter-helpers@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" - integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== - dependencies: - fast-diff "^1.1.2" - -prettier@^2.7.1: - version "2.7.1" - resolved "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz" - integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== - -pretty-format@^28.0.0, pretty-format@^28.1.3: - version "28.1.3" - resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz" - integrity sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q== - dependencies: - "@jest/schemas" "^28.1.3" - ansi-regex "^5.0.1" - ansi-styles "^5.0.0" - react-is "^18.0.0" - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -process-warning@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/process-warning/-/process-warning-2.0.0.tgz" - integrity sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" - integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== - -promise-inflight@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz" - integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== - -promise-retry@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz" - integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== - dependencies: - err-code "^2.0.2" - retry "^0.12.0" - -prompts@^2.0.1: - version "2.4.2" - resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" - integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" - -promzard@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz" - integrity sha512-JZeYqd7UAcHCwI+sTOeUDYkvEU+1bQ7iE0UT1MgB/tERkAPkesW46MrpIySzODi+owTjZtiF8Ay5j9m60KmMBw== - dependencies: - read "1" - -proto-list@~1.2.1: - version "1.2.4" - resolved "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz" - integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== - -protocols@^1.4.0: - version "1.4.8" - resolved "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz" - integrity sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg== - -protocols@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz" - integrity sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q== - -proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - -psl@^1.1.28: - version "1.9.0" - resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz" - integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@^1.3.2: - version "1.4.1" - resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" - integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== - -punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -q@^1.5.1: - version "1.5.1" - resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz" - integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== - -qs@6.10.3: - version "6.10.3" - resolved "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz" - integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== - dependencies: - side-channel "^1.0.4" - -qs@6.11.0, qs@^6.9.4: - version "6.11.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== - dependencies: - side-channel "^1.0.4" - -qs@~6.5.2: - version "6.5.3" - resolved "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz" - integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== - -query-string@^6.13.8: - version "6.14.1" - resolved "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz" - integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== - dependencies: - decode-uri-component "^0.2.0" - filter-obj "^1.1.0" - split-on-first "^1.0.0" - strict-uri-encode "^2.0.0" - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -quick-format-unescaped@^4.0.3: - version "4.0.4" - resolved "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz" - integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== - -quick-lru@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz" - integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== - -range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -rate-limiter-flexible@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/rate-limiter-flexible/-/rate-limiter-flexible-2.4.1.tgz#c74cfe36ac2cbfe56f68ded9a3b4b2fde1963c41" - integrity sha512-dgH4T44TzKVO9CLArNto62hJOwlWJMLUjVVr/ii0uUzZXEXthDNr7/yefW5z/1vvHAfycc1tnuiYyNJ8CTRB3g== - -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - -rc@^1.2.7: - version "1.2.8" - resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -react-is@^18.0.0: - version "18.2.0" - resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz" - integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== - -read-cmd-shim@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz" - integrity sha512-HJpV9bQpkl6KwjxlJcBoqu9Ba0PQg8TqSNIOrulGt54a0uup0HtevreFHzYzkm0lpnleRdNBzXznKrgxglEHQw== - -read-package-json-fast@^2.0.1: - version "2.0.3" - resolved "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz" - integrity sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ== - dependencies: - json-parse-even-better-errors "^2.3.0" - npm-normalize-package-bin "^1.0.1" - -read-package-json@^2.0.0: - version "2.1.2" - resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz" - integrity sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA== - dependencies: - glob "^7.1.1" - json-parse-even-better-errors "^2.3.0" - normalize-package-data "^2.0.0" - npm-normalize-package-bin "^1.0.0" - -read-package-json@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-3.0.1.tgz" - integrity sha512-aLcPqxovhJTVJcsnROuuzQvv6oziQx4zd3JvG0vGCL5MjTONUc4uJ90zCBC6R7W7oUKBNoR/F8pkyfVwlbxqng== - dependencies: - glob "^7.1.1" - json-parse-even-better-errors "^2.3.0" - normalize-package-data "^3.0.0" - npm-normalize-package-bin "^1.0.0" - -read-package-json@^4.1.1: - version "4.1.2" - resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-4.1.2.tgz" - integrity sha512-Dqer4pqzamDE2O4M55xp1qZMuLPqi4ldk2ya648FOMHRjwMzFhuxVrG04wd0c38IsvkVdr3vgHI6z+QTPdAjrQ== - dependencies: - glob "^7.1.1" - json-parse-even-better-errors "^2.3.0" - normalize-package-data "^3.0.0" - npm-normalize-package-bin "^1.0.0" - -read-package-tree@^5.3.1: - version "5.3.1" - resolved "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz" - integrity sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw== - dependencies: - read-package-json "^2.0.0" - readdir-scoped-modules "^1.0.0" - util-promisify "^2.1.0" - -read-pkg-up@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz" - integrity sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw== - dependencies: - find-up "^2.0.0" - read-pkg "^3.0.0" - -read-pkg-up@^7.0.1: - version "7.0.1" - resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz" - integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== - dependencies: - find-up "^4.1.0" - read-pkg "^5.2.0" - type-fest "^0.8.1" - -read-pkg@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz" - integrity sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA== - dependencies: - load-json-file "^4.0.0" - normalize-package-data "^2.3.2" - path-type "^3.0.0" - -read-pkg@^5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz" - integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== - dependencies: - "@types/normalize-package-data" "^2.4.0" - normalize-package-data "^2.5.0" - parse-json "^5.0.0" - type-fest "^0.6.0" - -read@1, read@~1.0.1: - version "1.0.7" - resolved "https://registry.npmjs.org/read/-/read-1.0.7.tgz" - integrity sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ== - dependencies: - mute-stream "~0.0.4" - -readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^2.0.6, readable-stream@~2.3.6: - version "2.3.7" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^4.0.0: - version "4.2.0" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-4.2.0.tgz" - integrity sha512-gJrBHsaI3lgBoGMW/jHZsQ/o/TIWiu5ENCJG1BB7fuCKzpFM8GaS2UoBVt9NO+oI+3FcrBNbUkl3ilDe09aY4A== - dependencies: - abort-controller "^3.0.0" - buffer "^6.0.3" - events "^3.3.0" - process "^0.11.10" - -readable-web-to-node-stream@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz#5d52bb5df7b54861fd48d015e93a2cb87b3ee0bb" - integrity sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw== - dependencies: - readable-stream "^3.6.0" - -readdir-scoped-modules@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz" - integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw== - dependencies: - debuglog "^1.0.1" - dezalgo "^1.0.0" - graceful-fs "^4.1.2" - once "^1.3.0" - -real-require@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz" - integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== - -redent@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz" - integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== - dependencies: - indent-string "^4.0.0" - strip-indent "^3.0.0" - -redis-errors@^1.0.0, redis-errors@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" - integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== - -redis-parser@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" - integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== - dependencies: - redis-errors "^1.0.0" - -regenerate-unicode-properties@^10.1.0: - version "10.1.0" - resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz" - integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== - dependencies: - regenerate "^1.4.2" - -regenerate@^1.4.2: - version "1.4.2" - resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.13.4: - version "0.13.9" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz" - integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== - -regenerator-transform@^0.15.0: - version "0.15.0" - resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.0.tgz" - integrity sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg== - dependencies: - "@babel/runtime" "^7.8.4" - -regexp.prototype.flags@^1.4.3: - version "1.4.3" - resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz" - integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - functions-have-names "^1.2.2" - -regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - -regexpu-core@^5.1.0: - version "5.2.1" - resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.1.tgz" - integrity sha512-HrnlNtpvqP1Xkb28tMhBUO2EbyUHdQlsnlAhzWcwHy8WJR53UWr7/MAvqrsQKMbV4qdpv03oTMG8iIhfsPFktQ== - dependencies: - regenerate "^1.4.2" - regenerate-unicode-properties "^10.1.0" - regjsgen "^0.7.1" - regjsparser "^0.9.1" - unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.0.0" - -regjsgen@^0.7.1: - version "0.7.1" - resolved "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz" - integrity sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA== - -regjsparser@^0.9.1: - version "0.9.1" - resolved "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz" - integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== - dependencies: - jsesc "~0.5.0" - -request@^2.88.0, request@^2.88.2: - version "2.88.2" - resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - -resolve-cwd@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" - integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== - dependencies: - resolve-from "^5.0.0" - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - -resolve.exports@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz" - integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== - -resolve@^1.10.0, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.20.0: - version "1.22.1" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== - dependencies: - is-core-module "^2.9.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" - integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rfdc@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" - integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== - -rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -rimraf@^3.0.0, rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -roarr@^7.0.4: - version "7.14.0" - resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.14.0.tgz#4de049c94aa930160a9e52ef3b0ebf166c5935b6" - integrity sha512-Np9LwC48JHvqQaS1lXvSi8aC+eaZSJItC3b0rNIan8jTwl8oTUMpqrcCvSO2bC6jDtqPI8zkQSVQWEpyl0L48Q== - dependencies: - boolean "^3.1.4" - fast-json-stringify "^2.7.10" - fast-printf "^1.6.9" - fast-safe-stringify "^2.1.1" - globalthis "^1.0.2" - semver-compare "^1.0.0" - -run-async@^2.4.0: - version "2.4.1" - resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" - integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -rxjs@^6.6.0: - version "6.6.7" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz" - integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== - dependencies: - tslib "^1.9.0" - -rxjs@^7.5.2: - version "7.6.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.6.0.tgz#361da5362b6ddaa691a2de0b4f2d32028f1eb5a2" - integrity sha512-DDa7d8TFNUalGC9VqXvQ1euWNN7sc63TrUCuM9J998+ViviahMIjKSOU7rfcgFOF+FCD71BhDRv4hrFz+ImDLQ== - dependencies: - tslib "^2.1.0" - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-regex-test@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" - integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" - is-regex "^1.1.4" - -safe-stable-stringify@^2.3.1: - version "2.4.0" - resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.0.tgz" - integrity sha512-eehKHKpab6E741ud7ZIMcXhKcP6TSIezPkNZhy5U8xC6+VvrRdUA2tMgxGxaGl4cz7c2Ew5+mg5+wNB16KQqrA== - -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -secure-json-parse@^2.4.0: - version "2.5.0" - resolved "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.5.0.tgz" - integrity sha512-ZQruFgZnIWH+WyO9t5rWt4ZEGqCKPwhiw+YbzTwpmT9elgLrLcfuyUiSnwwjUiVy9r4VM3urtbNF1xmEh9IL2w== - -semver-compare@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" - integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== - -"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: - version "5.7.1" - resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.1.1, semver@^7.1.3, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: - version "7.3.7" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz" - integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== - dependencies: - lru-cache "^6.0.0" - -semver@^7.3.8: - version "7.3.8" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== - dependencies: - lru-cache "^6.0.0" - -send@0.18.0: - version "0.18.0" - resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== - dependencies: - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "2.0.0" - mime "1.6.0" - ms "2.1.3" - on-finished "2.4.1" - range-parser "~1.2.1" - statuses "2.0.1" - -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.18.0" - -set-blocking@^2.0.0, set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" - integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - -sharp@^0.31.2: - version "0.31.2" - resolved "https://registry.npmjs.org/sharp/-/sharp-0.31.2.tgz" - integrity sha512-DUdNVEXgS5A97cTagSLIIp8dUZ/lZtk78iNVZgHdHbx1qnQR7JAHY0BnXnwwH39Iw+VKhO08CTYhIg0p98vQ5Q== - dependencies: - color "^4.2.3" - detect-libc "^2.0.1" - node-addon-api "^5.0.0" - prebuild-install "^7.1.1" - semver "^7.3.8" - simple-get "^4.0.1" - tar-fs "^2.1.1" - tunnel-agent "^0.6.0" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== - dependencies: - shebang-regex "^1.0.0" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -shell-quote@^1.6.1: - version "1.7.4" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.4.tgz#33fe15dee71ab2a81fcbd3a52106c5cfb9fb75d8" - integrity sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw== - -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== - dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" - -signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: - version "3.0.7" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -simple-concat@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz" - integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== - -simple-get@^4.0.0, simple-get@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz" - integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== - dependencies: - decompress-response "^6.0.0" - once "^1.3.1" - simple-concat "^1.0.0" - -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz" - integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== - dependencies: - is-arrayish "^0.3.1" - -sisteransi@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -slide@^1.1.6: - version "1.1.6" - resolved "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz" - integrity sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw== - -smart-buffer@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" - integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== - -socks-proxy-agent@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz" - integrity sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ== - dependencies: - agent-base "^6.0.2" - debug "4" - socks "^2.3.3" - -socks-proxy-agent@^6.0.0: - version "6.2.1" - resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz" - integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== - dependencies: - agent-base "^6.0.2" - debug "^4.3.3" - socks "^2.6.2" - -socks-proxy-agent@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6" - integrity sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww== - dependencies: - agent-base "^6.0.2" - debug "^4.3.3" - socks "^2.6.2" - -socks@^2.3.3, socks@^2.6.2: - version "2.7.0" - resolved "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz" - integrity sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA== - dependencies: - ip "^2.0.0" - smart-buffer "^4.2.0" - -sonic-boom@^3.0.0, sonic-boom@^3.1.0: - version "3.2.0" - resolved "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.2.0.tgz" - integrity sha512-SbbZ+Kqj/XIunvIAgUZRlqd6CGQYq71tRRbXR92Za8J/R3Yh4Av+TWENiSiEgnlwckYLyP0YZQWVfyNC0dzLaA== - dependencies: - atomic-sleep "^1.0.0" - -sort-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz" - integrity sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg== - dependencies: - is-plain-obj "^1.0.0" - -sort-keys@^4.0.0: - version "4.2.0" - resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-4.2.0.tgz" - integrity sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg== - dependencies: - is-plain-obj "^2.0.0" - -source-map-support@0.5.13: - version "0.5.13" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz" - integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0, source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.12" - resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz" - integrity sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA== - -split-on-first@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz" - integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== - -split2@^3.0.0: - version "3.2.2" - resolved "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz" - integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== - dependencies: - readable-stream "^3.0.0" - -split2@^4.0.0, split2@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz" - integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ== - -split@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/split/-/split-1.0.1.tgz" - integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== - dependencies: - through "2" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - -sshpk@^1.7.0: - version "1.17.0" - resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz" - integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -ssri@^8.0.0, ssri@^8.0.1: - version "8.0.1" - resolved "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz" - integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== - dependencies: - minipass "^3.1.1" - -ssri@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-9.0.1.tgz#544d4c357a8d7b71a19700074b6883fcb4eae057" - integrity sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q== - dependencies: - minipass "^3.1.1" - -stack-utils@^2.0.3: - version "2.0.5" - resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz" - integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== - dependencies: - escape-string-regexp "^2.0.0" - -standard-as-callback@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" - integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -stream-browserify@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" - integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== - dependencies: - inherits "~2.0.4" - readable-stream "^3.5.0" - -strict-uri-encode@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz" - integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== - -string-length@^4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" - integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== - dependencies: - char-regex "^1.0.2" - strip-ansi "^6.0.0" - -string-similarity@^4.0.1: - version "4.0.4" - resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b" - integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz" - integrity sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw== - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string.prototype.padend@^3.0.0: - version "3.1.4" - resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.4.tgz#2c43bb3a89eb54b6750de5942c123d6c98dd65b6" - integrity sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - -string.prototype.trimend@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz" - integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.19.5" - -string.prototype.trimend@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" - integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - -string.prototype.trimstart@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz" - integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.19.5" - -string.prototype.trimstart@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" - integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" - integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" - integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== - -strip-bom@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz" - integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -strip-indent@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz" - integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== - dependencies: - min-indent "^1.0.0" - -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" - integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== - -strnum@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz" - integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== - -strong-log-transformer@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz" - integrity sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA== - dependencies: - duplexer "^0.1.1" - minimist "^1.2.0" - through "^2.3.4" - -strtok3@^6.2.4: - version "6.3.0" - resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-6.3.0.tgz#358b80ffe6d5d5620e19a073aa78ce947a90f9a0" - integrity sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw== - dependencies: - "@tokenizer/token" "^0.3.0" - peek-readable "^4.1.0" - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.0.0, supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-hyperlinks@^2.0.0: - version "2.3.0" - resolved "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz" - integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== - dependencies: - has-flag "^4.0.0" - supports-color "^7.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -tar-fs@^2.0.0, tar-fs@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz" - integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.1.4" - -tar-stream@^2.1.4: - version "2.2.0" - resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -tar@^4.4.12: - version "4.4.19" - resolved "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz" - integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== - dependencies: - chownr "^1.1.4" - fs-minipass "^1.2.7" - minipass "^2.9.0" - minizlib "^1.3.3" - mkdirp "^0.5.5" - safe-buffer "^5.2.1" - yallist "^3.1.1" - -tar@^6.0.2, tar@^6.1.0: - version "6.1.11" - resolved "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz" - integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^3.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - -tar@^6.1.11, tar@^6.1.2: - version "6.1.14" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.14.tgz#e87926bec1cfe7c9e783a77a79f3e81c1cfa3b66" - integrity sha512-piERznXu0U7/pW7cdSn7hjqySIVTYT6F76icmFk7ptU7dDYlXTm5r9A6K04R2vU3olYgoKeo1Cg3eeu5nhftAw== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^5.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - -temp-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz" - integrity sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ== - -temp-write@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/temp-write/-/temp-write-4.0.0.tgz" - integrity sha512-HIeWmj77uOOHb0QX7siN3OtwV3CTntquin6TNVg6SHOqCP3hYKmox90eeFOGaY1MqJ9WYDDjkyZrW6qS5AWpbw== - dependencies: - graceful-fs "^4.1.15" - is-stream "^2.0.0" - make-dir "^3.0.0" - temp-dir "^1.0.0" - uuid "^3.3.2" - -terminal-link@^2.0.0: - version "2.1.1" - resolved "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz" - integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== - dependencies: - ansi-escapes "^4.2.1" - supports-hyperlinks "^2.0.0" - -test-exclude@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" - integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== - dependencies: - "@istanbuljs/schema" "^0.1.2" - glob "^7.1.4" - minimatch "^3.0.4" - -text-extensions@^1.0.0: - version "1.9.0" - resolved "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz" - integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - -thread-stream@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/thread-stream/-/thread-stream-2.2.0.tgz" - integrity sha512-rUkv4/fnb4rqy/gGy7VuqK6wE1+1DOCOWy4RMeaV69ZHMP11tQKZvZSip1yTgrKCMZzEMcCL/bKfHvSfDHx+iQ== - dependencies: - real-require "^0.2.0" - -through2@^2.0.0: - version "2.0.5" - resolved "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - -through2@^4.0.0: - version "4.0.2" - resolved "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz" - integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== - dependencies: - readable-stream "3" - -through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6: - version "2.3.8" - resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - -tlds@^1.234.0: - version "1.238.0" - resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.238.0.tgz#ffe7c19c8940c35b497cda187a6927f9450325a4" - integrity sha512-lFPF9pZFhLrPodaJ0wt9QIN0l8jOxqmUezGZnm7BfkDSVd9q667oVIJukLVzhF+4oW7uDlrLlfJrL5yu9RWwew== - -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -tmpl@1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" - integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -token-types@^4.1.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/token-types/-/token-types-4.2.1.tgz#0f897f03665846982806e138977dbe72d44df753" - integrity sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ== - dependencies: - "@tokenizer/token" "^0.3.0" - ieee754 "^1.2.1" - -tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - -tr46@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz" - integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== - dependencies: - punycode "^2.1.1" - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -trim-newlines@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz" - integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== - -ts-morph@^16.0.0: - version "16.0.0" - resolved "https://registry.npmjs.org/ts-morph/-/ts-morph-16.0.0.tgz" - integrity sha512-jGNF0GVpFj0orFw55LTsQxVYEUOCWBAbR5Ls7fTYE5pQsbW18ssTb/6UXx/GYAEjS+DQTp8VoTw0vqYMiaaQuw== - dependencies: - "@ts-morph/common" "~0.17.0" - code-block-writer "^11.0.3" - -ts-node@^10.8.1, ts-node@^10.8.2: - version "10.9.1" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz" - integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - -tslib@2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== - -tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0: - version "1.14.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslib@^2.1.0: - version "2.4.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz" - integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== - -tslib@^2.3.1: - version "2.4.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz" - integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== - -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" - integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" - integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -type-detect@4.0.8: - version "4.0.8" - resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" - integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== - -type-fest@^0.18.0: - version "0.18.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz" - integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - -type-fest@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.4.1.tgz" - integrity sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw== - -type-fest@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz" - integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== - -type-fest@^0.8.1: - version "0.8.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" - integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== - -type-fest@^2.3.3: - version "2.19.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" - integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== - -type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typed-emitter@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/typed-emitter/-/typed-emitter-2.1.0.tgz#ca78e3d8ef1476f228f548d62e04e3d4d3fd77fb" - integrity sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA== - optionalDependencies: - rxjs "^7.5.2" - -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" - integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== - -typescript@^4.8.4: - version "4.8.4" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz" - integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== - -uglify-js@^3.1.4: - version "3.17.1" - resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.1.tgz" - integrity sha512-+juFBsLLw7AqMaqJ0GFvlsGZwdQfI2ooKQB39PSBgMnMakcFosi9O8jCwE+2/2nMNcc0z63r9mwjoDG8zr+q0Q== - -uid-number@0.0.6: - version "0.0.6" - resolved "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz" - integrity sha512-c461FXIljswCuscZn67xq9PpszkPT6RjheWFQTgCyabJrTUozElanb0YEqv2UGgk247YpcJkFBuSGNvBlpXM9w== - -uint8arrays@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz" - integrity sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA== - dependencies: - multiformats "^9.4.2" - -umask@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz" - integrity sha512-lE/rxOhmiScJu9L6RTNVgB/zZbF+vGC0/p6D3xnkAePI2o0sMyFG966iR5Ki50OI/0mNi2yaRnxfLsPmEZF/JA== - -unbox-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" - integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== - dependencies: - call-bind "^1.0.2" - has-bigints "^1.0.2" - has-symbols "^1.0.3" - which-boxed-primitive "^1.0.2" - -unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== - -unicode-match-property-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz" - integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== - dependencies: - unicode-canonical-property-names-ecmascript "^2.0.0" - unicode-property-aliases-ecmascript "^2.0.0" - -unicode-match-property-value-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz" - integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== - -unicode-property-aliases-ecmascript@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz" - integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== - -unique-filename@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz" - integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== - dependencies: - unique-slug "^2.0.0" - -unique-filename@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-2.0.1.tgz#e785f8675a9a7589e0ac77e0b5c34d2eaeac6da2" - integrity sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A== - dependencies: - unique-slug "^3.0.0" - -unique-slug@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz" - integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== - dependencies: - imurmurhash "^0.1.4" - -unique-slug@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-3.0.0.tgz#6d347cf57c8a7a7a6044aabd0e2d74e4d76dc7c9" - integrity sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w== - dependencies: - imurmurhash "^0.1.4" - -universal-user-agent@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz" - integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== - -universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" - integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== - -upath@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz" - integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== - -update-browserslist-db@^1.0.9: - version "1.0.9" - resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz" - integrity sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -util-promisify@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/util-promisify/-/util-promisify-2.1.0.tgz" - integrity sha512-K+5eQPYs14b3+E+hmE2J6gCZ4JmMl9DbYS6BeP2CHq6WMuNxErxf5B/n0fz85L8zUuoO6rIzNNmIQDu/j+1OcA== - dependencies: - object.getownpropertydescriptors "^2.0.3" - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - -uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - -v8-to-istanbul@^9.0.1: - version "9.0.1" - resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz" - integrity sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w== - dependencies: - "@jridgewell/trace-mapping" "^0.3.12" - "@types/istanbul-lib-coverage" "^2.0.1" - convert-source-map "^1.6.0" - -validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -validate-npm-package-name@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz" - integrity sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw== - dependencies: - builtins "^1.0.3" - -varint@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0" - integrity sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg== - -vary@^1, vary@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" - integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" - integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -walker@^1.0.8: - version "1.0.8" - resolved "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz" - integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== - dependencies: - makeerror "1.0.12" - -wcwidth@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz" - integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== - dependencies: - defaults "^1.0.3" - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -webidl-conversions@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz" - integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -whatwg-url@^8.4.0: - version "8.7.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz" - integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== - dependencies: - lodash "^4.7.0" - tr46 "^2.1.0" - webidl-conversions "^6.1.0" - -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - -which@^1.2.9, which@^1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -which@^2.0.1, which@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.0, wide-align@^1.1.5: - version "1.1.5" - resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz" - integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== - dependencies: - string-width "^1.0.2 || 2 || 3 || 4" - -word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -wordwrap@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" - integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -write-file-atomic@^2.4.2: - version "2.4.3" - resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz" - integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== - dependencies: - graceful-fs "^4.1.11" - imurmurhash "^0.1.4" - signal-exit "^3.0.2" - -write-file-atomic@^3.0.0, write-file-atomic@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - -write-file-atomic@^4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz" - integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== - dependencies: - imurmurhash "^0.1.4" - signal-exit "^3.0.7" - -write-json-file@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/write-json-file/-/write-json-file-3.2.0.tgz" - integrity sha512-3xZqT7Byc2uORAatYiP3DHUUAVEkNOswEWNs9H5KXiicRTvzYzYqKjYc4G7p+8pltvAw641lVByKVtMpf+4sYQ== - dependencies: - detect-indent "^5.0.0" - graceful-fs "^4.1.15" - make-dir "^2.1.0" - pify "^4.0.1" - sort-keys "^2.0.0" - write-file-atomic "^2.4.2" - -write-json-file@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/write-json-file/-/write-json-file-4.3.0.tgz" - integrity sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ== - dependencies: - detect-indent "^6.0.0" - graceful-fs "^4.1.15" - is-plain-obj "^2.0.0" - make-dir "^3.0.0" - sort-keys "^4.0.0" - write-file-atomic "^3.0.0" - -write-pkg@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/write-pkg/-/write-pkg-4.0.0.tgz" - integrity sha512-v2UQ+50TNf2rNHJ8NyWttfm/EJUBWMJcx6ZTYZr6Qp52uuegWw/lBkCtCbnYZEmPRNL61m+u67dAmGxo+HTULA== - dependencies: - sort-keys "^2.0.0" - type-fest "^0.4.1" - write-json-file "^3.2.0" - -ws@^8.12.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.0.tgz#485074cc392689da78e1828a9ff23585e06cddd8" - integrity sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig== - -xtend@^4.0.0, xtend@~4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yallist@^3.0.0, yallist@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@^1.10.0: - version "1.10.2" - resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yargs-parser@20.2.4: - version "20.2.4" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz" - integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== - -yargs-parser@^20.2.2, yargs-parser@^20.2.3: - version "20.2.9" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - -yargs-parser@^21.0.0: - version "21.1.1" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - -yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - -yargs@^17.3.1: - version "17.5.1" - resolved "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz" - integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.0.0" - -yesno@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/yesno/-/yesno-0.4.0.tgz" - integrity sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA== - -yn@3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -zod@^3.14.2: - version "3.19.1" - resolved "https://registry.npmjs.org/zod/-/zod-3.19.1.tgz" - integrity sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA== - -zod@^3.21.4: - version "3.21.4" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" - integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== From 90b0a9d809f342557429e30d1319fe98782db75e Mon Sep 17 00:00:00 2001 From: David Buchanan Date: Wed, 6 Sep 2023 16:08:06 +0100 Subject: [PATCH 216/237] RFC: Support image aspect ratio metadata (#1306) * Add aspectRatio field to embed image lexicon * lexicon codegen * pass aspectRatio through to imagesEmbed views * simplify aspectRatio logic, port to bsky package * re-run lexicon codegen --- lexicons/app/bsky/embed/images.json | 15 +++++++++-- packages/api/src/client/lexicons.ts | 24 +++++++++++++++++ .../src/client/types/app/bsky/embed/images.ts | 21 +++++++++++++++ packages/bsky/src/lexicon/lexicons.ts | 27 +++++++++++++++++++ .../lexicon/types/app/bsky/embed/images.ts | 21 +++++++++++++++ .../com/atproto/temp/upgradeRepoVersion.ts | 1 + packages/bsky/src/services/feed/views.ts | 1 + .../pds/src/app-view/services/feed/views.ts | 1 + packages/pds/src/lexicon/lexicons.ts | 24 +++++++++++++++++ .../lexicon/types/app/bsky/embed/images.ts | 21 +++++++++++++++ 10 files changed, 154 insertions(+), 2 deletions(-) diff --git a/lexicons/app/bsky/embed/images.json b/lexicons/app/bsky/embed/images.json index 148fd2493ae..732b0d9a4e8 100644 --- a/lexicons/app/bsky/embed/images.json +++ b/lexicons/app/bsky/embed/images.json @@ -23,7 +23,17 @@ "accept": ["image/*"], "maxSize": 1000000 }, - "alt": {"type": "string"} + "alt": {"type": "string"}, + "aspectRatio": {"type": "ref", "ref": "#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": { @@ -43,7 +53,8 @@ "properties": { "thumb": {"type": "string"}, "fullsize": {"type": "string"}, - "alt": {"type": "string"} + "alt": {"type": "string"}, + "aspectRatio": {"type": "ref", "ref": "#aspectRatio"} } } } diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 90b0c4eb475..e15d7aba1ce 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -4145,6 +4145,26 @@ export const schemaDict = { 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: { @@ -4174,6 +4194,10 @@ export const schemaDict = { alt: { type: 'string', }, + aspectRatio: { + type: 'ref', + ref: 'lex:app.bsky.embed.images#aspectRatio', + }, }, }, }, diff --git a/packages/api/src/client/types/app/bsky/embed/images.ts b/packages/api/src/client/types/app/bsky/embed/images.ts index f98dc3f64db..77909a4b3b0 100644 --- a/packages/api/src/client/types/app/bsky/embed/images.ts +++ b/packages/api/src/client/types/app/bsky/embed/images.ts @@ -27,6 +27,7 @@ export function validateMain(v: unknown): ValidationResult { export interface Image { image: BlobRef alt: string + aspectRatio?: AspectRatio [k: string]: unknown } @@ -40,6 +41,25 @@ 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 @@ -59,6 +79,7 @@ export interface ViewImage { thumb: string fullsize: string alt: string + aspectRatio?: AspectRatio [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 76283a33dd3..e15d7aba1ce 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -3536,6 +3536,9 @@ export const schemaDict = { type: 'string', format: 'did', }, + force: { + type: 'boolean', + }, }, }, }, @@ -4142,6 +4145,26 @@ export const schemaDict = { 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: { @@ -4171,6 +4194,10 @@ export const schemaDict = { alt: { type: 'string', }, + aspectRatio: { + type: 'ref', + ref: 'lex:app.bsky.embed.images#aspectRatio', + }, }, }, }, diff --git a/packages/bsky/src/lexicon/types/app/bsky/embed/images.ts b/packages/bsky/src/lexicon/types/app/bsky/embed/images.ts index 422b036fa70..4864fad3dea 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/embed/images.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/embed/images.ts @@ -27,6 +27,7 @@ export function validateMain(v: unknown): ValidationResult { export interface Image { image: BlobRef alt: string + aspectRatio?: AspectRatio [k: string]: unknown } @@ -40,6 +41,25 @@ 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 @@ -59,6 +79,7 @@ export interface ViewImage { thumb: string fullsize: string alt: string + aspectRatio?: AspectRatio [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts b/packages/bsky/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts index 13e68eb5c7d..c5b77876fec 100644 --- a/packages/bsky/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts +++ b/packages/bsky/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts @@ -12,6 +12,7 @@ export interface QueryParams {} export interface InputSchema { did: string + force?: boolean [k: string]: unknown } diff --git a/packages/bsky/src/services/feed/views.ts b/packages/bsky/src/services/feed/views.ts index c040f96522c..dc19ddf637e 100644 --- a/packages/bsky/src/services/feed/views.ts +++ b/packages/bsky/src/services/feed/views.ts @@ -260,6 +260,7 @@ export class FeedViews { img.image.ref, ), alt: img.alt, + aspectRatio: img.aspectRatio, })) return { $type: 'app.bsky.embed.images#view', diff --git a/packages/pds/src/app-view/services/feed/views.ts b/packages/pds/src/app-view/services/feed/views.ts index 1897542a310..cfb4d158e2b 100644 --- a/packages/pds/src/app-view/services/feed/views.ts +++ b/packages/pds/src/app-view/services/feed/views.ts @@ -258,6 +258,7 @@ export class FeedViews { img.image.ref, ), alt: img.alt, + aspectRatio: img.aspectRatio, })) return { $type: 'app.bsky.embed.images#view', diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 90b0c4eb475..e15d7aba1ce 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -4145,6 +4145,26 @@ export const schemaDict = { 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: { @@ -4174,6 +4194,10 @@ export const schemaDict = { alt: { type: 'string', }, + aspectRatio: { + type: 'ref', + ref: 'lex:app.bsky.embed.images#aspectRatio', + }, }, }, }, diff --git a/packages/pds/src/lexicon/types/app/bsky/embed/images.ts b/packages/pds/src/lexicon/types/app/bsky/embed/images.ts index 422b036fa70..4864fad3dea 100644 --- a/packages/pds/src/lexicon/types/app/bsky/embed/images.ts +++ b/packages/pds/src/lexicon/types/app/bsky/embed/images.ts @@ -27,6 +27,7 @@ export function validateMain(v: unknown): ValidationResult { export interface Image { image: BlobRef alt: string + aspectRatio?: AspectRatio [k: string]: unknown } @@ -40,6 +41,25 @@ 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 @@ -59,6 +79,7 @@ export interface ViewImage { thumb: string fullsize: string alt: string + aspectRatio?: AspectRatio [k: string]: unknown } From a948d53969375d6bd810dcd97ad073962941374d Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 6 Sep 2023 10:08:18 -0500 Subject: [PATCH 217/237] Only do read after write on own author feed (#1544) only munge on own feed --- .../app-view/api/app/bsky/feed/getAuthorFeed.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts index bc559e1e894..24f511bf99c 100644 --- a/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/pds/src/app-view/api/app/bsky/feed/getAuthorFeed.ts @@ -8,6 +8,7 @@ import { OutputSchema } from '../../../../../lexicon/types/app/bsky/feed/getAuth import { handleReadAfterWrite } from '../util/read-after-write' import { authPassthru } from '../../../../../api/com/atproto/admin/util' import { LocalRecords } from '../../../../../services/local' +import { isReasonRepost } from '../../../../../lexicon/types/app/bsky/feed/defs' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getAuthorFeed({ @@ -147,6 +148,10 @@ const getAuthorMunge = async ( ): Promise => { const localSrvc = ctx.services.local(ctx.db) const localProf = local.profile + // only munge on own feed + if (!isUsersFeed(original, requester)) { + return original + } let feed = original.feed // first update any out of date profile pictures in feed if (localProf) { @@ -173,3 +178,12 @@ const getAuthorMunge = async ( feed, } } + +const isUsersFeed = (feed: OutputSchema, requester: string) => { + const first = feed.feed.at(0) + if (!first) return false + if (!first.reason && first.post.author.did === requester) return true + if (isReasonRepost(first.reason) && first.reason.by.did === requester) + return true + return false +} From f023cfb009e2bbe695a86ec8cc3a972e642a18ce Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Wed, 6 Sep 2023 17:35:39 -0500 Subject: [PATCH 218/237] Abyss Integration (#1545) * add blob scanner * introduce auto moderator * tidy * tests * more tests & tidy * timeout & logs * tweak cfg * push takedowns to pds * build branch * update snaps * add cloudfront invalidator to indexer * tweak log * dont build branch --- packages/bsky/src/auto-moderator/abyss.ts | 106 +++++++++ .../src/{labeler => auto-moderator}/hive.ts | 25 +- packages/bsky/src/auto-moderator/index.ts | 216 ++++++++++++++++++ packages/bsky/src/auto-moderator/keyword.ts | 25 ++ .../src/{labeler => auto-moderator}/util.ts | 28 +-- packages/bsky/src/indexer/config.ts | 35 ++- packages/bsky/src/indexer/context.ts | 6 + packages/bsky/src/indexer/index.ts | 38 +-- packages/bsky/src/indexer/services.ts | 8 +- packages/bsky/src/labeler/base.ts | 98 -------- packages/bsky/src/labeler/index.ts | 3 - packages/bsky/src/labeler/keyword.ts | 38 --- packages/bsky/src/services/indexing/index.ts | 12 +- packages/bsky/src/services/label/index.ts | 6 +- .../fixtures/hiveai_resp_example.json | 0 .../{labeler => auto-moderator}/hive.test.ts | 4 +- .../labeler.test.ts | 41 +--- .../tests/auto-moderator/takedowns.test.ts | 168 ++++++++++++++ packages/dev-env/src/bsky.ts | 8 + packages/dev-env/src/network.ts | 2 + packages/dev-env/src/types.ts | 3 + .../__snapshots__/feedgen.test.ts.snap | 10 +- .../proxied/__snapshots__/views.test.ts.snap | 30 ++- services/bsky/indexer.js | 11 +- 24 files changed, 671 insertions(+), 250 deletions(-) create mode 100644 packages/bsky/src/auto-moderator/abyss.ts rename packages/bsky/src/{labeler => auto-moderator}/hive.ts (89%) create mode 100644 packages/bsky/src/auto-moderator/index.ts create mode 100644 packages/bsky/src/auto-moderator/keyword.ts rename packages/bsky/src/{labeler => auto-moderator}/util.ts (82%) delete mode 100644 packages/bsky/src/labeler/base.ts delete mode 100644 packages/bsky/src/labeler/index.ts delete mode 100644 packages/bsky/src/labeler/keyword.ts rename packages/bsky/tests/{labeler => auto-moderator}/fixtures/hiveai_resp_example.json (100%) rename packages/bsky/tests/{labeler => auto-moderator}/hive.test.ts (78%) rename packages/bsky/tests/{labeler => auto-moderator}/labeler.test.ts (82%) create mode 100644 packages/bsky/tests/auto-moderator/takedowns.test.ts diff --git a/packages/bsky/src/auto-moderator/abyss.ts b/packages/bsky/src/auto-moderator/abyss.ts new file mode 100644 index 00000000000..6d9cb6afa37 --- /dev/null +++ b/packages/bsky/src/auto-moderator/abyss.ts @@ -0,0 +1,106 @@ +import axios from 'axios' +import { CID } from 'multiformats/cid' +import * as ui8 from 'uint8arrays' +import { resolveBlob } from '../api/blob-resolver' +import { retryHttp } from '../util/retry' +import { PrimaryDatabase } from '../db' +import { IdResolver } from '@atproto/identity' +import { labelerLogger as log } from '../logger' + +export interface TakedownFlagger { + scanImage(did: string, cid: CID): Promise +} + +export class Abyss implements TakedownFlagger { + protected auth: string + + constructor( + public endpoint: string, + protected password: string, + public ctx: { db: PrimaryDatabase; idResolver: IdResolver }, + ) { + this.auth = basicAuth(this.password) + } + + async scanImage(did: string, cid: CID): Promise { + const start = Date.now() + const res = await retryHttp(async () => { + try { + return await this.makeReq(did, cid) + } catch (err) { + log.warn({ err, did, cid: cid.toString() }, 'abyss request failed') + throw err + } + }) + log.info( + { res, did, cid: cid.toString(), duration: Date.now() - start }, + 'abyss response', + ) + return this.parseRes(res) + } + + async makeReq(did: string, cid: CID): Promise { + const { stream, contentType } = await resolveBlob( + did, + cid, + this.ctx.db, + this.ctx.idResolver, + ) + const { data } = await axios.post(this.getReqUrl({ did }), stream, { + headers: { + 'Content-Type': contentType, + authorization: this.auth, + }, + timeout: 10000, + }) + return data + } + + parseRes(res: ScannerResp): string[] { + if (!res.match || res.match.status !== 'success') { + return [] + } + const labels: string[] = [] + for (const hit of res.match.hits) { + if (TAKEDOWN_LABELS.includes(hit.label)) { + labels.push(hit.label) + } + } + return labels + } + + getReqUrl(params: { did: string }) { + return `${this.endpoint}/xrpc/com.atproto.unspecced.scanBlob?did=${params.did}` + } +} + +const TAKEDOWN_LABELS = ['csam', 'csem'] + +type ScannerResp = { + blob: unknown + match?: { + status: string + hits: ScannerHit[] + } + classify?: { + hits?: unknown[] + } + review?: { + state?: string + ticketId?: string + } +} + +type ScannerHit = { + hashType: string + hashValue: string + label: string + corpus: string +} + +const basicAuth = (password: string) => { + return ( + 'Basic ' + + ui8.toString(ui8.fromString(`admin:${password}`, 'utf8'), 'base64pad') + ) +} diff --git a/packages/bsky/src/labeler/hive.ts b/packages/bsky/src/auto-moderator/hive.ts similarity index 89% rename from packages/bsky/src/labeler/hive.ts rename to packages/bsky/src/auto-moderator/hive.ts index 3088504ae83..51d67c1c783 100644 --- a/packages/bsky/src/labeler/hive.ts +++ b/packages/bsky/src/auto-moderator/hive.ts @@ -2,38 +2,25 @@ import axios from 'axios' import FormData from 'form-data' import { CID } from 'multiformats/cid' import { IdResolver } from '@atproto/identity' -import { Labeler } from './base' -import { keywordLabeling } from './util' import { PrimaryDatabase } from '../db' -import { BackgroundQueue } from '../background' -import { IndexerConfig } from '../indexer/config' import { retryHttp } from '../util/retry' import { resolveBlob } from '../api/blob-resolver' import { labelerLogger as log } from '../logger' const HIVE_ENDPOINT = 'https://api.thehive.ai/api/v2/task/sync' -export class HiveLabeler extends Labeler { - hiveApiKey: string - keywords: Record +export interface ImgLabeler { + labelImg(did: string, cid: CID): Promise +} +export class HiveLabeler implements ImgLabeler { constructor( - hiveApiKey: string, + public hiveApiKey: string, protected ctx: { db: PrimaryDatabase idResolver: IdResolver - cfg: IndexerConfig - backgroundQueue: BackgroundQueue }, - ) { - super(ctx) - this.hiveApiKey = hiveApiKey - this.keywords = ctx.cfg.labelerKeywords - } - - async labelText(text: string): Promise { - return keywordLabeling(this.keywords, text) - } + ) {} async labelImg(did: string, cid: CID): Promise { const hiveRes = await retryHttp(async () => { diff --git a/packages/bsky/src/auto-moderator/index.ts b/packages/bsky/src/auto-moderator/index.ts new file mode 100644 index 00000000000..a2d041d17f3 --- /dev/null +++ b/packages/bsky/src/auto-moderator/index.ts @@ -0,0 +1,216 @@ +import { AtUri } from '@atproto/syntax' +import { AtpAgent } from '@atproto/api' +import { dedupe, getFieldsFromRecord } from './util' +import { labelerLogger as log } from '../logger' +import { PrimaryDatabase } from '../db' +import { IdResolver } from '@atproto/identity' +import { BackgroundQueue } from '../background' +import { IndexerConfig } from '../indexer/config' +import { buildBasicAuth } from '../auth' +import { CID } from 'multiformats/cid' +import { LabelService } from '../services/label' +import { ModerationService } from '../services/moderation' +import { TakedownFlagger } 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' + +export class AutoModerator { + public pushAgent?: AtpAgent + public takedownFlagger?: TakedownFlagger + 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) { + this.takedownFlagger = new Abyss(abyssEndpoint, abyssPassword, ctx) + } else { + log.error( + { abyssEndpoint, abyssPassword }, + 'abyss not properly configured', + ) + } + + 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), + ) + } + } + + processRecord(uri: AtUri, cid: CID, obj: unknown) { + this.ctx.backgroundQueue.add(async () => { + const { text, imgs } = getFieldsFromRecord(obj) + await Promise.all([ + this.labelRecord(uri, cid, text, imgs).catch((err) => { + log.error( + { err, uri: uri.toString(), record: obj }, + 'failed to label record', + ) + }), + this.checkImgForTakedown(uri, cid, imgs).catch((err) => { + log.error( + { err, uri: uri.toString(), record: obj }, + 'failed to check img for takedown', + ) + }), + ]) + }) + } + + async labelRecord(uri: AtUri, recordCid: CID, text: string[], imgs: CID[]) { + if (uri.collection === ids.AppBskyActorProfile) { + // @TODO label profiles + return + } + const allLabels = await Promise.all([ + this.textLabeler?.labelText(text.join(' ')), + ...imgs.map((cid) => this.imgLabeler?.labelImg(uri.host, cid)), + ]) + const labels = dedupe(allLabels.flat()) + await this.storeLabels(uri, recordCid, labels) + } + + async checkImgForTakedown(uri: AtUri, recordCid: CID, imgCids: CID[]) { + if (imgCids.length < 0) return + const results = await Promise.all( + imgCids.map((cid) => this.takedownFlagger?.scanImage(uri.host, cid)), + ) + const takedownCids: CID[] = [] + for (let i = 0; i < results.length; i++) { + if (results.at(i)?.length) { + takedownCids.push(imgCids[i]) + } + } + if (takedownCids.length === 0) return + try { + await this.persistTakedown( + uri, + recordCid, + takedownCids, + dedupe(results.flat()), + ) + } catch (err) { + log.error( + { + err, + uri: uri.toString(), + imgCids: imgCids.map((c) => c.toString()), + results, + }, + 'failed to persist takedown', + ) + } + } + + async persistTakedown( + uri: AtUri, + recordCid: CID, + takedownCids: CID[], + labels: string[], + ) { + const reason = `automated takedown for labels: ${labels.join(', ')}` + if (this.pushAgent) { + await this.pushAgent.com.atproto.admin.takeModerationAction({ + action: 'com.atproto.admin.defs#takedown', + subject: { + $type: 'com.atproto.repo.strongRef', + uri: uri.toString(), + cid: recordCid.toString(), + }, + subjectBlobCids: takedownCids.map((c) => c.toString()), + reason, + 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.logAction({ + action: 'com.atproto.admin.defs#takedown', + subject: { uri, cid: recordCid }, + subjectBlobCids: takedownCids, + reason, + createdBy: this.ctx.cfg.labelerDid, + }) + await modSrvc.takedownRecord({ + takedownId: action.id, + uri: uri, + blobCids: takedownCids, + }) + }) + } + } + + async storeLabels(uri: AtUri, cid: CID, labels: string[]): Promise { + if (labels.length < 1) return + const labelSrvc = this.services.label(this.ctx.db) + const formatted = await labelSrvc.formatAndCreate( + this.ctx.cfg.labelerDid, + uri.toString(), + cid.toString(), + { create: labels }, + ) + if (this.pushAgent) { + const agent = this.pushAgent + try { + await agent.api.app.bsky.unspecced.applyLabels({ labels: formatted }) + } catch (err) { + log.error( + { + err, + uri: uri.toString(), + labels, + receiver: agent.service.toString(), + }, + 'failed to push labels', + ) + } + } + } + + async processAll() { + await this.ctx.backgroundQueue.processAll() + } +} diff --git a/packages/bsky/src/auto-moderator/keyword.ts b/packages/bsky/src/auto-moderator/keyword.ts new file mode 100644 index 00000000000..6bc504aa142 --- /dev/null +++ b/packages/bsky/src/auto-moderator/keyword.ts @@ -0,0 +1,25 @@ +export interface TextLabeler { + labelText(text: string): Promise +} + +export class KeywordLabeler implements TextLabeler { + constructor(public keywords: Record) {} + + async labelText(text: string): Promise { + return keywordLabeling(this.keywords, text) + } +} + +export const keywordLabeling = ( + keywords: Record, + text: string, +): string[] => { + const lowerText = text.toLowerCase() + const labels: string[] = [] + for (const word of Object.keys(keywords)) { + if (lowerText.includes(word)) { + labels.push(keywords[word]) + } + } + return labels +} diff --git a/packages/bsky/src/labeler/util.ts b/packages/bsky/src/auto-moderator/util.ts similarity index 82% rename from packages/bsky/src/labeler/util.ts rename to packages/bsky/src/auto-moderator/util.ts index 4175886a542..ba49eb2a9f3 100644 --- a/packages/bsky/src/labeler/util.ts +++ b/packages/bsky/src/auto-moderator/util.ts @@ -14,9 +14,8 @@ type RecordFields = { export const getFieldsFromRecord = (record: unknown): RecordFields => { if (isPost(record)) { return getFieldsFromPost(record) - // @TODO add back in profile labeling - // } else if (isProfile(record)) { - // return getFieldsFromProfile(record) + } else if (isProfile(record)) { + return getFieldsFromProfile(record) } else { return { text: [], imgs: [] } } @@ -62,8 +61,13 @@ export const getFieldsFromProfile = (record: ProfileRecord): RecordFields => { return { text, imgs } } -export const dedupe = (str: string[]): string[] => { - const set = new Set(str) +export const dedupe = (strs: (string | undefined)[]): string[] => { + const set = new Set() + for (const str of strs) { + if (str !== undefined) { + set.add(str) + } + } return [...set] } @@ -84,20 +88,6 @@ export const isRecordType = (obj: unknown, lexId: string): boolean => { } } -export const keywordLabeling = ( - keywords: Record, - text: string, -): string[] => { - const lowerText = text.toLowerCase() - const labels: string[] = [] - for (const word of Object.keys(keywords)) { - if (lowerText.includes(word)) { - labels.push(keywords[word]) - } - } - return labels -} - const separateEmbeds = (embed: PostRecord['embed']) => { if (!embed) { return [] diff --git a/packages/bsky/src/indexer/config.ts b/packages/bsky/src/indexer/config.ts index 37c04af6cb3..579cfd24432 100644 --- a/packages/bsky/src/indexer/config.ts +++ b/packages/bsky/src/indexer/config.ts @@ -15,8 +15,11 @@ export interface IndexerConfigValues { handleResolveNameservers?: string[] labelerDid: string hiveApiKey?: string + abyssEndpoint?: string + abyssPassword?: string + imgUriEndpoint?: string labelerKeywords: Record - labelerPushUrl?: string + moderationPushUrl?: string indexerConcurrency?: number indexerPartitionIds: number[] indexerPartitionBatchSize?: number @@ -62,9 +65,14 @@ export class IndexerConfig { ? process.env.HANDLE_RESOLVE_NAMESERVERS.split(',') : [] const labelerDid = process.env.LABELER_DID || 'did:example:labeler' - const labelerPushUrl = - overrides?.labelerPushUrl || process.env.LABELER_PUSH_URL || undefined + const moderationPushUrl = + overrides?.moderationPushUrl || + process.env.MODERATION_PUSH_URL || + undefined const hiveApiKey = process.env.HIVE_API_KEY || undefined + const abyssEndpoint = process.env.ABYSS_ENDPOINT + const abyssPassword = process.env.ABYSS_PASSWORD + const imgUriEndpoint = process.env.IMG_URI_ENDPOINT const indexerPartitionIds = overrides?.indexerPartitionIds || (process.env.INDEXER_PARTITION_IDS @@ -99,8 +107,11 @@ export class IndexerConfig { didCacheMaxTTL, handleResolveNameservers, labelerDid, - labelerPushUrl, + moderationPushUrl, hiveApiKey, + abyssEndpoint, + abyssPassword, + imgUriEndpoint, indexerPartitionIds, indexerConcurrency, indexerPartitionBatchSize, @@ -162,14 +173,26 @@ export class IndexerConfig { return this.cfg.labelerDid } - get labelerPushUrl() { - return this.cfg.labelerPushUrl + get moderationPushUrl() { + return this.cfg.moderationPushUrl } get hiveApiKey() { return this.cfg.hiveApiKey } + get abyssEndpoint() { + return this.cfg.abyssEndpoint + } + + get abyssPassword() { + return this.cfg.abyssPassword + } + + get imgUriEndpoint() { + return this.cfg.imgUriEndpoint + } + get indexerConcurrency() { return this.cfg.indexerConcurrency } diff --git a/packages/bsky/src/indexer/context.ts b/packages/bsky/src/indexer/context.ts index 34cc43a227d..e7fe24580fa 100644 --- a/packages/bsky/src/indexer/context.ts +++ b/packages/bsky/src/indexer/context.ts @@ -5,6 +5,7 @@ import { Services } from './services' import { BackgroundQueue } from '../background' import DidSqlCache from '../did-cache' import { Redis } from '../redis' +import { AutoModerator } from '../auto-moderator' export class IndexerContext { constructor( @@ -16,6 +17,7 @@ export class IndexerContext { idResolver: IdResolver didCache: DidSqlCache backgroundQueue: BackgroundQueue + autoMod: AutoModerator }, ) {} @@ -46,6 +48,10 @@ export class IndexerContext { get backgroundQueue(): BackgroundQueue { return this.opts.backgroundQueue } + + get autoMod(): AutoModerator { + return this.opts.autoMod + } } export default IndexerContext diff --git a/packages/bsky/src/indexer/index.ts b/packages/bsky/src/indexer/index.ts index 3e955b53351..ed8188d353b 100644 --- a/packages/bsky/src/indexer/index.ts +++ b/packages/bsky/src/indexer/index.ts @@ -9,10 +9,12 @@ import { IndexerConfig } from './config' import { IndexerContext } from './context' import { createServices } from './services' import { IndexerSubscription } from './subscription' -import { HiveLabeler, KeywordLabeler, Labeler } from '../labeler' +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' @@ -39,6 +41,7 @@ export class BskyIndexer { db: PrimaryDatabase redis: Redis cfg: IndexerConfig + imgInvalidator?: ImageInvalidator }): BskyIndexer { const { db, redis, cfg } = opts const didCache = new DidSqlCache( @@ -52,28 +55,26 @@ export class BskyIndexer { backupNameservers: cfg.handleResolveNameservers, }) const backgroundQueue = new BackgroundQueue(db) - let labeler: Labeler - if (cfg.hiveApiKey) { - labeler = new HiveLabeler(cfg.hiveApiKey, { - db, - cfg, - idResolver, - backgroundQueue, - }) - } else { - labeler = new KeywordLabeler({ - db, - cfg, - idResolver, - backgroundQueue, - }) - } + + 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 ? new NotificationServer(db, cfg.pushNotificationEndpoint) : undefined const services = createServices({ idResolver, - labeler, + autoMod, backgroundQueue, notifServer, }) @@ -85,6 +86,7 @@ export class BskyIndexer { idResolver, didCache, backgroundQueue, + autoMod, }) const sub = new IndexerSubscription(ctx, { partitionIds: cfg.indexerPartitionIds, diff --git a/packages/bsky/src/indexer/services.ts b/packages/bsky/src/indexer/services.ts index 0e6908de099..df173352046 100644 --- a/packages/bsky/src/indexer/services.ts +++ b/packages/bsky/src/indexer/services.ts @@ -1,22 +1,22 @@ import { IdResolver } from '@atproto/identity' import { PrimaryDatabase } from '../db' -import { Labeler } from '../labeler' import { BackgroundQueue } from '../background' import { IndexingService } from '../services/indexing' import { LabelService } from '../services/label' import { NotificationServer } from '../notifications' +import { AutoModerator } from '../auto-moderator' export function createServices(resources: { idResolver: IdResolver - labeler: Labeler + autoMod: AutoModerator backgroundQueue: BackgroundQueue notifServer?: NotificationServer }): Services { - const { idResolver, labeler, backgroundQueue, notifServer } = resources + const { idResolver, autoMod, backgroundQueue, notifServer } = resources return { indexing: IndexingService.creator( idResolver, - labeler, + autoMod, backgroundQueue, notifServer, ), diff --git a/packages/bsky/src/labeler/base.ts b/packages/bsky/src/labeler/base.ts deleted file mode 100644 index f9efd457fdb..00000000000 --- a/packages/bsky/src/labeler/base.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { AtUri } from '@atproto/syntax' -import { AtpAgent } from '@atproto/api' -import { cidForRecord } from '@atproto/repo' -import { dedupe, getFieldsFromRecord } from './util' -import { labelerLogger as log } from '../logger' -import { PrimaryDatabase } from '../db' -import { IdResolver } from '@atproto/identity' -import { BackgroundQueue } from '../background' -import { IndexerConfig } from '../indexer/config' -import { buildBasicAuth } from '../auth' -import { CID } from 'multiformats/cid' - -export abstract class Labeler { - public backgroundQueue: BackgroundQueue - public pushAgent?: AtpAgent - constructor( - protected ctx: { - db: PrimaryDatabase - idResolver: IdResolver - cfg: IndexerConfig - backgroundQueue: BackgroundQueue - }, - ) { - this.backgroundQueue = ctx.backgroundQueue - if (ctx.cfg.labelerPushUrl) { - const url = new URL(ctx.cfg.labelerPushUrl) - this.pushAgent = new AtpAgent({ service: url.origin }) - this.pushAgent.api.setHeader( - 'authorization', - buildBasicAuth(url.username, url.password), - ) - } - } - - processRecord(uri: AtUri, obj: unknown) { - this.backgroundQueue.add(() => - this.createAndStoreLabels(uri, obj).catch((err) => { - log.error( - { err, uri: uri.toString(), record: obj }, - 'failed to label record', - ) - }), - ) - } - - async createAndStoreLabels(uri: AtUri, obj: unknown): Promise { - const labels = await this.labelRecord(uri, obj) - if (labels.length < 1) return - const cid = await cidForRecord(obj) - const rows = labels.map((val) => ({ - src: this.ctx.cfg.labelerDid, - uri: uri.toString(), - cid: cid.toString(), - val, - neg: false, - cts: new Date().toISOString(), - })) - - await this.ctx.db.db - .insertInto('label') - .values(rows) - .onConflict((oc) => oc.doNothing()) - .execute() - - if (this.pushAgent) { - const agent = this.pushAgent - try { - await agent.api.app.bsky.unspecced.applyLabels({ labels: rows }) - } catch (err) { - log.error( - { - err, - uri: uri.toString(), - labels, - receiver: agent.service.toString(), - }, - 'failed to push labels', - ) - } - } - } - - async labelRecord(uri: AtUri, obj: unknown): Promise { - const { text, imgs } = getFieldsFromRecord(obj) - const txtLabels = await this.labelText(text.join(' ')) - const imgLabels = await Promise.all( - imgs.map((cid) => this.labelImg(uri.host, cid)), - ) - return dedupe([...txtLabels, ...imgLabels.flat()]) - } - - abstract labelText(text: string): Promise - abstract labelImg(did: string, cid: CID): Promise - - async processAll() { - await this.backgroundQueue.processAll() - } -} diff --git a/packages/bsky/src/labeler/index.ts b/packages/bsky/src/labeler/index.ts deleted file mode 100644 index cd6d2a64345..00000000000 --- a/packages/bsky/src/labeler/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './base' -export * from './hive' -export * from './keyword' diff --git a/packages/bsky/src/labeler/keyword.ts b/packages/bsky/src/labeler/keyword.ts deleted file mode 100644 index 395bb15278e..00000000000 --- a/packages/bsky/src/labeler/keyword.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { PrimaryDatabase } from '../db' -import { Labeler } from './base' -import { getFieldsFromRecord, keywordLabeling } from './util' -import { IdResolver } from '@atproto/identity' -import { BackgroundQueue } from '../background' -import { AtUri } from '@atproto/syntax' -import { IndexerConfig } from '../indexer/config' - -export class KeywordLabeler extends Labeler { - keywords: Record - - constructor( - protected ctx: { - db: PrimaryDatabase - idResolver: IdResolver - cfg: IndexerConfig - backgroundQueue: BackgroundQueue - }, - ) { - super(ctx) - this.keywords = ctx.cfg.labelerKeywords - } - - async labelRecord(_uri: AtUri, obj: unknown): Promise { - // skip image resolution - const { text } = getFieldsFromRecord(obj) - const txtLabels = await this.labelText(text.join(' ')) - return txtLabels - } - - async labelText(text: string): Promise { - return keywordLabeling(this.keywords, text) - } - - async labelImg(): Promise { - return [] - } -} diff --git a/packages/bsky/src/services/indexing/index.ts b/packages/bsky/src/services/indexing/index.ts index 2af633cce29..ff979ee034f 100644 --- a/packages/bsky/src/services/indexing/index.ts +++ b/packages/bsky/src/services/indexing/index.ts @@ -25,9 +25,9 @@ import * as FeedGenerator from './plugins/feed-generator' import RecordProcessor from './processor' import { subLogger } from '../../logger' import { retryHttp } from '../../util/retry' -import { Labeler } from '../../labeler' import { BackgroundQueue } from '../../background' import { NotificationServer } from '../../notifications' +import { AutoModerator } from '../../auto-moderator' import { Actor } from '../../db/tables/actor' export class IndexingService { @@ -46,7 +46,7 @@ export class IndexingService { constructor( public db: PrimaryDatabase, public idResolver: IdResolver, - public labeler: Labeler, + public autoMod: AutoModerator, public backgroundQueue: BackgroundQueue, public notifServer?: NotificationServer, ) { @@ -72,7 +72,7 @@ export class IndexingService { return new IndexingService( txn, this.idResolver, - this.labeler, + this.autoMod, this.backgroundQueue, this.notifServer, ) @@ -80,12 +80,12 @@ export class IndexingService { static creator( idResolver: IdResolver, - labeler: Labeler, + autoMod: AutoModerator, backgroundQueue: BackgroundQueue, notifServer?: NotificationServer, ) { return (db: PrimaryDatabase) => - new IndexingService(db, idResolver, labeler, backgroundQueue, notifServer) + new IndexingService(db, idResolver, autoMod, backgroundQueue, notifServer) } async indexRecord( @@ -108,7 +108,7 @@ export class IndexingService { } }) if (!opts?.disableLabels) { - this.labeler.processRecord(uri, obj) + this.autoMod.processRecord(uri, cid, obj) } } diff --git a/packages/bsky/src/services/label/index.ts b/packages/bsky/src/services/label/index.ts index 42b263138ab..bfd037e8fb8 100644 --- a/packages/bsky/src/services/label/index.ts +++ b/packages/bsky/src/services/label/index.ts @@ -20,7 +20,7 @@ export class LabelService { uri: string, cid: string | null, labels: { create?: string[]; negate?: string[] }, - ) { + ): Promise { const { create = [], negate = [] } = labels const toCreate = create.map((val) => ({ src, @@ -38,7 +38,9 @@ export class LabelService { neg: true, cts: new Date().toISOString(), })) - await this.createLabels([...toCreate, ...toNegate]) + const formatted = [...toCreate, ...toNegate] + await this.createLabels(formatted) + return formatted } async createLabels(labels: Label[]) { diff --git a/packages/bsky/tests/labeler/fixtures/hiveai_resp_example.json b/packages/bsky/tests/auto-moderator/fixtures/hiveai_resp_example.json similarity index 100% rename from packages/bsky/tests/labeler/fixtures/hiveai_resp_example.json rename to packages/bsky/tests/auto-moderator/fixtures/hiveai_resp_example.json diff --git a/packages/bsky/tests/labeler/hive.test.ts b/packages/bsky/tests/auto-moderator/hive.test.ts similarity index 78% rename from packages/bsky/tests/labeler/hive.test.ts rename to packages/bsky/tests/auto-moderator/hive.test.ts index 3213d794e30..3a5cef45a37 100644 --- a/packages/bsky/tests/labeler/hive.test.ts +++ b/packages/bsky/tests/auto-moderator/hive.test.ts @@ -1,10 +1,10 @@ import fs from 'fs/promises' -import * as hive from '../../src/labeler/hive' +import * as hive from '../../src/auto-moderator/hive' describe('labeling', () => { it('correctly parses hive responses', async () => { const exampleRespBytes = await fs.readFile( - 'tests/labeler/fixtures/hiveai_resp_example.json', + 'tests/auto-moderator/fixtures/hiveai_resp_example.json', ) const exampleResp = JSON.parse(exampleRespBytes.toString()) const classes = hive.respToClasses(exampleResp) diff --git a/packages/bsky/tests/labeler/labeler.test.ts b/packages/bsky/tests/auto-moderator/labeler.test.ts similarity index 82% rename from packages/bsky/tests/labeler/labeler.test.ts rename to packages/bsky/tests/auto-moderator/labeler.test.ts index 18d0e5e0daa..7227e769549 100644 --- a/packages/bsky/tests/labeler/labeler.test.ts +++ b/packages/bsky/tests/auto-moderator/labeler.test.ts @@ -1,18 +1,15 @@ import { AtUri, AtpAgent, BlobRef } from '@atproto/api' import { Readable } from 'stream' -import { Labeler } from '../../src/labeler' -import { Database, IndexerConfig } from '../../src' +import { AutoModerator } from '../../src/auto-moderator' import IndexerContext from '../../src/indexer/context' import { cidForRecord } from '@atproto/repo' -import { keywordLabeling } from '../../src/labeler/util' import { cidForCbor, TID } from '@atproto/common' import { LabelService } from '../../src/services/label' import { TestNetwork } from '@atproto/dev-env' -import { IdResolver } from '@atproto/identity' import { SeedClient } from '../seeds/client' import usersSeed from '../seeds/users' -import { BackgroundQueue } from '../../src/background' import { CID } from 'multiformats/cid' +import { ImgLabeler } from '../../src/auto-moderator/hive' // outside of test suite so that TestLabeler can access them let badCid1: CID | undefined = undefined @@ -20,7 +17,7 @@ let badCid2: CID | undefined = undefined describe('labeler', () => { let network: TestNetwork - let labeler: Labeler + let autoMod: AutoModerator let labelSrvc: LabelService let ctx: IndexerContext let labelerDid: string @@ -37,7 +34,8 @@ describe('labeler', () => { ctx = network.bsky.indexer.ctx const pdsCtx = network.pds.ctx labelerDid = ctx.cfg.labelerDid - labeler = new TestLabeler(network.bsky.indexer.ctx) + autoMod = ctx.autoMod + autoMod.imgLabeler = new TestImgLabeler() labelSrvc = ctx.services.label(ctx.db) const pdsAgent = new AtpAgent({ service: network.pds.url }) const sc = new SeedClient(pdsAgent) @@ -87,8 +85,8 @@ describe('labeler', () => { } const cid = await cidForRecord(post) const uri = postUri() - labeler.processRecord(uri, post) - await labeler.processAll() + autoMod.processRecord(uri, cid, post) + await autoMod.processAll() const labels = await labelSrvc.getLabels(uri.toString()) expect(labels.length).toBe(1) expect(labels[0]).toMatchObject({ @@ -124,8 +122,9 @@ describe('labeler', () => { createdAt: new Date().toISOString(), } const uri = postUri() - labeler.processRecord(uri, post) - await labeler.processAll() + const cid = await cidForRecord(post) + autoMod.processRecord(uri, cid, post) + await autoMod.processAll() const dbLabels = await labelSrvc.getLabels(uri.toString()) const labels = dbLabels.map((row) => row.val).sort() expect(labels).toEqual( @@ -158,25 +157,7 @@ describe('labeler', () => { }) }) -class TestLabeler extends Labeler { - hiveApiKey: string - keywords: Record - - constructor(opts: { - db: Database - idResolver: IdResolver - cfg: IndexerConfig - backgroundQueue: BackgroundQueue - }) { - const { db, cfg, idResolver, backgroundQueue } = opts - super({ db, cfg, idResolver, backgroundQueue }) - this.keywords = cfg.labelerKeywords - } - - async labelText(text: string): Promise { - return keywordLabeling(this.keywords, text) - } - +class TestImgLabeler implements ImgLabeler { async labelImg(_did: string, cid: CID): Promise { if (cid.equals(badCid1)) { return ['img-label'] diff --git a/packages/bsky/tests/auto-moderator/takedowns.test.ts b/packages/bsky/tests/auto-moderator/takedowns.test.ts new file mode 100644 index 00000000000..0fa4cbf730c --- /dev/null +++ b/packages/bsky/tests/auto-moderator/takedowns.test.ts @@ -0,0 +1,168 @@ +import fs from 'fs/promises' +import { AtpAgent } from '@atproto/api' +import { AutoModerator } from '../../src/auto-moderator' +import IndexerContext from '../../src/indexer/context' +import { sha256RawToCid } from '@atproto/common' +import { TestNetwork } from '@atproto/dev-env' +import { ImageRef, SeedClient } from '../seeds/client' +import usersSeed from '../seeds/users' +import { CID } from 'multiformats/cid' +import { TakedownFlagger } from '../../src/auto-moderator/abyss' +import { ImageInvalidator } from '../../src/image/invalidator' +import { sha256 } from '@atproto/crypto' +import { ids } from '../../src/lexicon/lexicons' + +// outside of test suite so that TestLabeler can access them +let badCid1: CID | undefined = undefined +let badCid2: CID | undefined = undefined + +describe('takedowner', () => { + let network: TestNetwork + let autoMod: AutoModerator + let testInvalidator: TestInvalidator + let ctx: IndexerContext + let pdsAgent: AtpAgent + let sc: SeedClient + let alice: string + let badBlob1: ImageRef + let badBlob2: ImageRef + let goodBlob: ImageRef + + beforeAll(async () => { + testInvalidator = new TestInvalidator() + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_automod_takedown', + bsky: { + imgInvalidator: testInvalidator, + }, + }) + ctx = network.bsky.indexer.ctx + autoMod = ctx.autoMod + autoMod.takedownFlagger = new TestFlagger() + pdsAgent = new AtpAgent({ service: network.pds.url }) + sc = new SeedClient(pdsAgent) + await usersSeed(sc) + await network.processAll() + alice = sc.dids.alice + const fileBytes1 = await fs.readFile( + 'tests/image/fixtures/key-portrait-small.jpg', + ) + const fileBytes2 = await fs.readFile( + 'tests/image/fixtures/key-portrait-large.jpg', + ) + badCid1 = sha256RawToCid(await sha256(fileBytes1)) + badCid2 = sha256RawToCid(await sha256(fileBytes2)) + goodBlob = await sc.uploadFile( + alice, + 'tests/image/fixtures/key-landscape-small.jpg', + 'image/jpeg', + ) + badBlob1 = await sc.uploadFile( + alice, + 'tests/image/fixtures/key-portrait-small.jpg', + 'image/jpeg', + ) + badBlob2 = await sc.uploadFile( + alice, + 'tests/image/fixtures/key-portrait-large.jpg', + 'image/jpeg', + ) + }) + + afterAll(async () => { + await network.close() + }) + + 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 modAction = await ctx.db.db + .selectFrom('moderation_action') + .where('subjectUri', '=', post.ref.uriStr) + .select(['action', 'id']) + .executeTakeFirst() + if (!modAction) { + throw new Error('expected mod action') + } + expect(modAction.action).toEqual('com.atproto.admin.defs#takedown') + const record = await ctx.db.db + .selectFrom('record') + .where('uri', '=', post.ref.uriStr) + .select('takedownId') + .executeTakeFirst() + expect(record?.takedownId).toEqual(modAction.id) + + const recordPds = await network.pds.ctx.db.db + .selectFrom('record') + .where('uri', '=', post.ref.uriStr) + .select('takedownId') + .executeTakeFirst() + expect(recordPds?.takedownId).toEqual(modAction.id) + + expect(testInvalidator.invalidated.length).toBe(1) + expect(testInvalidator.invalidated[0].subject).toBe( + badBlob1.image.ref.toString(), + ) + }) + + it('takes down flagged content in profiles', async () => { + const res = await pdsAgent.api.com.atproto.repo.putRecord( + { + repo: alice, + collection: ids.AppBskyActorProfile, + rkey: 'self', + record: { + avatar: badBlob2.image, + }, + }, + { headers: sc.getHeaders(alice), encoding: 'application/json' }, + ) + await network.processAll() + const modAction = await ctx.db.db + .selectFrom('moderation_action') + .where('subjectUri', '=', res.data.uri) + .select(['action', 'id']) + .executeTakeFirst() + if (!modAction) { + throw new Error('expected mod action') + } + expect(modAction.action).toEqual('com.atproto.admin.defs#takedown') + const record = await ctx.db.db + .selectFrom('record') + .where('uri', '=', res.data.uri) + .select('takedownId') + .executeTakeFirst() + expect(record?.takedownId).toEqual(modAction.id) + + const recordPds = await network.pds.ctx.db.db + .selectFrom('record') + .where('uri', '=', res.data.uri) + .select('takedownId') + .executeTakeFirst() + expect(recordPds?.takedownId).toEqual(modAction.id) + + expect(testInvalidator.invalidated.length).toBe(2) + expect(testInvalidator.invalidated[1].subject).toBe( + badBlob2.image.ref.toString(), + ) + }) +}) + +class TestInvalidator implements ImageInvalidator { + public invalidated: { subject: string; paths: string[] }[] = [] + async invalidate(subject: string, paths: string[]) { + this.invalidated.push({ subject, paths }) + } +} + +class TestFlagger implements TakedownFlagger { + async scanImage(_did: string, cid: CID): Promise { + if (cid.equals(badCid1)) { + return ['kill-it'] + } else if (cid.equals(badCid2)) { + return ['with-fire'] + } + return [] + } +} diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index 336e16ec6a6..15ea03375e2 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -77,6 +77,7 @@ export class TestBsky { db, config, algos: cfg.algos, + imgInvalidator: cfg.imgInvalidator, }) // indexer const ns = cfg.dbPostgresSchema @@ -92,6 +93,9 @@ export class TestBsky { dbPostgresSchema: cfg.dbPostgresSchema, didPlcUrl: cfg.plcUrl, labelerKeywords: { label_me: 'test-label', label_me_2: 'test-label-2' }, + abyssEndpoint: '', + abyssPassword: '', + moderationPushUrl: `http://admin:${config.adminPassword}@localhost:${cfg.pdsPort}`, indexerPartitionIds: [0], indexerNamespace: `ns${ns}`, indexerSubLockId: uniqueLockId(), @@ -108,6 +112,7 @@ export class TestBsky { cfg: indexerCfg, db: db.getPrimary(), redis: indexerRedis, + imgInvalidator: cfg.imgInvalidator, }) // ingester const ingesterCfg = new bsky.IngesterConfig({ @@ -237,6 +242,9 @@ export async function getIndexers( dbPostgresUrl: process.env.DB_POSTGRES_URL || '', dbPostgresSchema: `appview_${name}`, didPlcUrl: network.plc.url, + imgUriEndpoint: '', + abyssEndpoint: '', + abyssPassword: '', indexerPartitionIds: [0], indexerNamespace: `ns${ns}`, ingesterPartitionCount: config.ingesterPartitionCount ?? 1, diff --git a/packages/dev-env/src/network.ts b/packages/dev-env/src/network.ts index 39d9041e031..7b332c1d81c 100644 --- a/packages/dev-env/src/network.ts +++ b/packages/dev-env/src/network.ts @@ -35,6 +35,7 @@ export class TestNetwork extends TestNetworkNoAppView { const bsky = await TestBsky.create({ port: bskyPort, plcUrl: plc.url, + pdsPort, repoProvider: `ws://localhost:${pdsPort}`, dbPostgresSchema: `appview_${dbPostgresSchema}`, dbPrimaryPostgresUrl: dbPostgresUrl, @@ -48,6 +49,7 @@ export class TestNetwork extends TestNetworkNoAppView { plcUrl: plc.url, bskyAppViewEndpoint: bsky.url, bskyAppViewDid: bsky.ctx.cfg.serverDid, + bskyAppViewModeration: true, ...params.pds, }) diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index cd7d029debc..ef8f357ee73 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -1,5 +1,6 @@ import * as pds from '@atproto/pds' import * as bsky from '@atproto/bsky' +import { ImageInvalidator } from '@atproto/bsky/src/image/invalidator' export type PlcConfig = { port?: number @@ -19,6 +20,8 @@ export type BskyConfig = Partial & { repoProvider: string dbPrimaryPostgresUrl: string redisHost: string + pdsPort: number + imgInvalidator?: ImageInvalidator migration?: string algos?: bsky.MountedAlgos } diff --git a/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap index 0f9ce0b75eb..400f405fed3 100644 --- a/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/feedgen.test.ts.snap @@ -560,7 +560,15 @@ Object { "author": Object { "did": "user(5)", "handle": "dan.test", - "labels": Array [], + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(5)", + "val": "repo-action-label", + }, + ], "viewer": Object { "blockedBy": false, "following": "record(12)", diff --git a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap index 65baff8fa50..6a055da07a2 100644 --- a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap @@ -150,7 +150,15 @@ Object { Object { "did": "user(0)", "handle": "dan.test", - "labels": Array [], + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(0)", + "val": "repo-action-label", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -239,7 +247,15 @@ Array [ Object { "did": "user(5)", "handle": "dan.test", - "labels": Array [], + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(5)", + "val": "repo-action-label", + }, + ], "viewer": Object { "blockedBy": false, "following": "record(6)", @@ -2211,7 +2227,15 @@ Object { Object { "did": "user(2)", "handle": "dan.test", - "labels": Array [], + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(2)", + "val": "repo-action-label", + }, + ], "viewer": Object { "blockedBy": false, "following": "record(3)", diff --git a/services/bsky/indexer.js b/services/bsky/indexer.js index a2e28dcb114..6cd390c9f43 100644 --- a/services/bsky/indexer.js +++ b/services/bsky/indexer.js @@ -3,6 +3,7 @@ require('dd-trace/init') // Only works with commonjs // Tracer code above must come before anything else +const { CloudfrontInvalidator } = require('@atproto/aws') const { IndexerConfig, BskyIndexer, @@ -24,6 +25,12 @@ const main = async () => { dbPostgresUrl: env.dbPostgresUrl, dbPostgresSchema: env.dbPostgresSchema, }) + const cfInvalidator = env.cfDistributionId + ? new CloudfrontInvalidator({ + distributionId: env.cfDistributionId, + pathPrefix: cfg.imgUriEndpoint && new URL(cfg.imgUriEndpoint).pathname, + }) + : undefined const redis = new Redis( cfg.redisSentinelName ? { @@ -36,7 +43,7 @@ const main = async () => { password: cfg.redisPassword, }, ) - const indexer = BskyIndexer.create({ db, redis, cfg }) + const indexer = BskyIndexer.create({ db, redis, cfg, imgInvalidator: cfInvalidator }) await indexer.start() process.on('SIGTERM', async () => { await indexer.destroy() @@ -65,6 +72,8 @@ const getEnv = () => ({ dbPoolSize: maybeParseInt(process.env.DB_POOL_SIZE), dbPoolMaxUses: maybeParseInt(process.env.DB_POOL_MAX_USES), dbPoolIdleTimeoutMs: maybeParseInt(process.env.DB_POOL_IDLE_TIMEOUT_MS), + cfDistributionId: process.env.CF_DISTRIBUTION_ID, + imgUriEndpoint: process.env.IMG_URI_ENDPOINT, }) const maybeParseInt = (str) => { From b3eb3d76a07797cef078a4a8042739ddfa196e9e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 6 Sep 2023 18:37:54 -0500 Subject: [PATCH 219/237] Add Changesets (#1513) * add changesets * clean up scripts * remove test changeset * only build containers on push to production, clean up other workflows * keep building from main * remove production branch for now --- .changeset/README.md | 8 + .changeset/config.json | 11 + .github/workflows/publish.yaml | 34 ++ .github/workflows/repo.yaml | 9 +- package.json | 7 +- pnpm-lock.yaml | 798 +++++++++++++++++++++++++++++++++ 6 files changed, 864 insertions(+), 3 deletions(-) create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json create mode 100644 .github/workflows/publish.yaml diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 00000000000..e5b6d8d6a67 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 00000000000..98267c52238 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", + "changelog": ["@changesets/changelog-github", { "repo": "bluesky-social/atproto" }], + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 00000000000..4e93a6df1ba --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,34 @@ +name: Publish + +on: + push: + branches: + - main + +env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + build: + name: Build & Publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + with: + version: 8 + - uses: actions/setup-node@v3 + with: + node-version: 18 + cache: "pnpm" + - run: pnpm i --frozen-lockfile + - run: pnpm verify + - name: Publish + id: changesets + uses: changesets/action@v1 + with: + publish: pnpm release + version: pnpm version-packages + commit: "Version packages" + title: "Version packages" diff --git a/.github/workflows/repo.yaml b/.github/workflows/repo.yaml index 8f25afdcbbe..7fbe3b9f739 100644 --- a/.github/workflows/repo.yaml +++ b/.github/workflows/repo.yaml @@ -1,14 +1,17 @@ -name: repo +name: Test + on: pull_request: - push: branches: - main + concurrency: group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}' cancel-in-progress: true + jobs: build: + name: Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -22,6 +25,7 @@ jobs: - run: pnpm i --frozen-lockfile - run: pnpm build test: + name: Test strategy: matrix: shard: [1/8, 2/8, 3/8, 4/8, 5/8, 6/8, 7/8, 8/8] @@ -38,6 +42,7 @@ jobs: - run: pnpm i --frozen-lockfile - run: pnpm test:withFlags --maxWorkers=1 --shard=${{ matrix.shard }} --passWithNoTests verify: + name: Verify runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/package.json b/package.json index c13995d6ca6..7a6b3ac6fb9 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,16 @@ "build": "pnpm -r --stream build", "update-main-to-dist": "pnpm -r --stream update-main-to-dist", "test": "LOG_ENABLED=false NODE_ENV=development ./packages/dev-infra/with-test-redis-and-db.sh pnpm --stream -r test", - "test:withFlags": "LOG_ENABLED=false NODE_ENV=development ./packages/dev-infra/with-test-redis-and-db.sh pnpm --stream -r test --" + "test:withFlags": "LOG_ENABLED=false NODE_ENV=development ./packages/dev-infra/with-test-redis-and-db.sh pnpm --stream -r test --", + "changeset": "changeset", + "release": "pnpm build && changeset publish", + "version-packages": "changeset version && git add ." }, "devDependencies": { "@babel/core": "^7.18.6", "@babel/preset-env": "^7.18.6", + "@changesets/changelog-github": "^0.4.8", + "@changesets/cli": "^2.26.2", "@npmcli/package-json": "^3.0.0", "@swc/core": "^1.3.42", "@swc/jest": "^0.2.24", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b18c4765c4a..cf41041ac50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,12 @@ importers: '@babel/preset-env': specifier: ^7.18.6 version: 7.18.6(@babel/core@7.18.6) + '@changesets/changelog-github': + specifier: ^0.4.8 + version: 0.4.8 + '@changesets/cli': + specifier: ^2.26.2 + version: 2.26.2 '@npmcli/package-json': specifier: ^3.0.0 version: 3.0.0 @@ -4314,6 +4320,209 @@ packages: dev: false optional: true + /@changesets/apply-release-plan@6.1.4: + resolution: {integrity: sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==} + dependencies: + '@babel/runtime': 7.22.10 + '@changesets/config': 2.3.1 + '@changesets/get-version-range-type': 0.3.2 + '@changesets/git': 2.0.0 + '@changesets/types': 5.2.1 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.7.1 + resolve-from: 5.0.0 + semver: 7.5.4 + dev: true + + /@changesets/assemble-release-plan@5.2.4: + resolution: {integrity: sha512-xJkWX+1/CUaOUWTguXEbCDTyWJFECEhmdtbkjhn5GVBGxdP/JwaHBIU9sW3FR6gD07UwZ7ovpiPclQZs+j+mvg==} + dependencies: + '@babel/runtime': 7.22.10 + '@changesets/errors': 0.1.4 + '@changesets/get-dependents-graph': 1.3.6 + '@changesets/types': 5.2.1 + '@manypkg/get-packages': 1.1.3 + semver: 7.5.4 + dev: true + + /@changesets/changelog-git@0.1.14: + resolution: {integrity: sha512-+vRfnKtXVWsDDxGctOfzJsPhaCdXRYoe+KyWYoq5X/GqoISREiat0l3L8B0a453B2B4dfHGcZaGyowHbp9BSaA==} + dependencies: + '@changesets/types': 5.2.1 + dev: true + + /@changesets/changelog-github@0.4.8: + resolution: {integrity: sha512-jR1DHibkMAb5v/8ym77E4AMNWZKB5NPzw5a5Wtqm1JepAuIF+hrKp2u04NKM14oBZhHglkCfrla9uq8ORnK/dw==} + dependencies: + '@changesets/get-github-info': 0.5.2 + '@changesets/types': 5.2.1 + dotenv: 8.6.0 + transitivePeerDependencies: + - encoding + dev: true + + /@changesets/cli@2.26.2: + resolution: {integrity: sha512-dnWrJTmRR8bCHikJHl9b9HW3gXACCehz4OasrXpMp7sx97ECuBGGNjJhjPhdZNCvMy9mn4BWdplI323IbqsRig==} + hasBin: true + dependencies: + '@babel/runtime': 7.22.10 + '@changesets/apply-release-plan': 6.1.4 + '@changesets/assemble-release-plan': 5.2.4 + '@changesets/changelog-git': 0.1.14 + '@changesets/config': 2.3.1 + '@changesets/errors': 0.1.4 + '@changesets/get-dependents-graph': 1.3.6 + '@changesets/get-release-plan': 3.0.17 + '@changesets/git': 2.0.0 + '@changesets/logger': 0.0.5 + '@changesets/pre': 1.0.14 + '@changesets/read': 0.5.9 + '@changesets/types': 5.2.1 + '@changesets/write': 0.2.3 + '@manypkg/get-packages': 1.1.3 + '@types/is-ci': 3.0.0 + '@types/semver': 7.5.0 + ansi-colors: 4.1.3 + chalk: 2.4.2 + enquirer: 2.4.1 + external-editor: 3.1.0 + fs-extra: 7.0.1 + human-id: 1.0.2 + is-ci: 3.0.1 + meow: 6.1.1 + outdent: 0.5.0 + p-limit: 2.3.0 + preferred-pm: 3.0.3 + resolve-from: 5.0.0 + semver: 7.5.4 + spawndamnit: 2.0.0 + term-size: 2.2.1 + tty-table: 4.2.1 + dev: true + + /@changesets/config@2.3.1: + resolution: {integrity: sha512-PQXaJl82CfIXddUOppj4zWu+987GCw2M+eQcOepxN5s+kvnsZOwjEJO3DH9eVy+OP6Pg/KFEWdsECFEYTtbg6w==} + dependencies: + '@changesets/errors': 0.1.4 + '@changesets/get-dependents-graph': 1.3.6 + '@changesets/logger': 0.0.5 + '@changesets/types': 5.2.1 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.5 + dev: true + + /@changesets/errors@0.1.4: + resolution: {integrity: sha512-HAcqPF7snsUJ/QzkWoKfRfXushHTu+K5KZLJWPb34s4eCZShIf8BFO3fwq6KU8+G7L5KdtN2BzQAXOSXEyiY9Q==} + dependencies: + extendable-error: 0.1.7 + dev: true + + /@changesets/get-dependents-graph@1.3.6: + resolution: {integrity: sha512-Q/sLgBANmkvUm09GgRsAvEtY3p1/5OCzgBE5vX3vgb5CvW0j7CEljocx5oPXeQSNph6FXulJlXV3Re/v3K3P3Q==} + dependencies: + '@changesets/types': 5.2.1 + '@manypkg/get-packages': 1.1.3 + chalk: 2.4.2 + fs-extra: 7.0.1 + semver: 7.5.4 + dev: true + + /@changesets/get-github-info@0.5.2: + resolution: {integrity: sha512-JppheLu7S114aEs157fOZDjFqUDpm7eHdq5E8SSR0gUBTEK0cNSHsrSR5a66xs0z3RWuo46QvA3vawp8BxDHvg==} + dependencies: + dataloader: 1.4.0 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: true + + /@changesets/get-release-plan@3.0.17: + resolution: {integrity: sha512-6IwKTubNEgoOZwDontYc2x2cWXfr6IKxP3IhKeK+WjyD6y3M4Gl/jdQvBw+m/5zWILSOCAaGLu2ZF6Q+WiPniw==} + dependencies: + '@babel/runtime': 7.22.10 + '@changesets/assemble-release-plan': 5.2.4 + '@changesets/config': 2.3.1 + '@changesets/pre': 1.0.14 + '@changesets/read': 0.5.9 + '@changesets/types': 5.2.1 + '@manypkg/get-packages': 1.1.3 + dev: true + + /@changesets/get-version-range-type@0.3.2: + resolution: {integrity: sha512-SVqwYs5pULYjYT4op21F2pVbcrca4qA/bAA3FmFXKMN7Y+HcO8sbZUTx3TAy2VXulP2FACd1aC7f2nTuqSPbqg==} + dev: true + + /@changesets/git@2.0.0: + resolution: {integrity: sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==} + dependencies: + '@babel/runtime': 7.22.10 + '@changesets/errors': 0.1.4 + '@changesets/types': 5.2.1 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.5 + spawndamnit: 2.0.0 + dev: true + + /@changesets/logger@0.0.5: + resolution: {integrity: sha512-gJyZHomu8nASHpaANzc6bkQMO9gU/ib20lqew1rVx753FOxffnCrJlGIeQVxNWCqM+o6OOleCo/ivL8UAO5iFw==} + dependencies: + chalk: 2.4.2 + dev: true + + /@changesets/parse@0.3.16: + resolution: {integrity: sha512-127JKNd167ayAuBjUggZBkmDS5fIKsthnr9jr6bdnuUljroiERW7FBTDNnNVyJ4l69PzR57pk6mXQdtJyBCJKg==} + dependencies: + '@changesets/types': 5.2.1 + js-yaml: 3.14.1 + dev: true + + /@changesets/pre@1.0.14: + resolution: {integrity: sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==} + dependencies: + '@babel/runtime': 7.22.10 + '@changesets/errors': 0.1.4 + '@changesets/types': 5.2.1 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + dev: true + + /@changesets/read@0.5.9: + resolution: {integrity: sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==} + dependencies: + '@babel/runtime': 7.22.10 + '@changesets/git': 2.0.0 + '@changesets/logger': 0.0.5 + '@changesets/parse': 0.3.16 + '@changesets/types': 5.2.1 + chalk: 2.4.2 + fs-extra: 7.0.1 + p-filter: 2.1.0 + dev: true + + /@changesets/types@4.1.0: + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + dev: true + + /@changesets/types@5.2.1: + resolution: {integrity: sha512-myLfHbVOqaq9UtUKqR/nZA/OY7xFjQMdfgfqeZIBK4d0hA6pgxArvdv8M+6NUzzBsjWLOtvApv8YHr4qM+Kpfg==} + dev: true + + /@changesets/write@0.2.3: + resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==} + dependencies: + '@babel/runtime': 7.22.10 + '@changesets/types': 5.2.1 + fs-extra: 7.0.1 + human-id: 1.0.2 + prettier: 2.7.1 + dev: true + /@cspotcode/source-map-support@0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -4759,6 +4968,26 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /@manypkg/find-root@1.1.0: + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + dependencies: + '@babel/runtime': 7.22.10 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + dev: true + + /@manypkg/get-packages@1.1.3: + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + dependencies: + '@babel/runtime': 7.22.10 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + dev: true + /@noble/curves@1.1.0: resolution: {integrity: sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==} dependencies: @@ -5127,6 +5356,12 @@ packages: resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==} dev: true + /@types/is-ci@3.0.0: + resolution: {integrity: sha512-Q0Op0hdWbYd1iahB+IFNQcWXFq4O0Q5MwQP7uN0souuQ4rPg1vEYcnIOfr1gY+M+6rc8FGoRaBO1mOOvL29sEQ==} + dependencies: + ci-info: 3.8.0 + dev: true + /@types/istanbul-lib-coverage@2.0.4: resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} dev: true @@ -5168,6 +5403,14 @@ packages: resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} dev: true + /@types/minimist@1.2.2: + resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} + dev: true + + /@types/node@12.20.55: + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + dev: true + /@types/node@18.0.0: resolution: {integrity: sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==} dev: true @@ -5181,6 +5424,10 @@ packages: '@types/node': 18.17.8 dev: true + /@types/normalize-package-data@2.4.1: + resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} + dev: true + /@types/pg@8.6.6: resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==} dependencies: @@ -5201,6 +5448,10 @@ packages: resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} dev: true + /@types/semver@7.5.0: + resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} + dev: true + /@types/send@0.17.1: resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} dependencies: @@ -5468,6 +5719,11 @@ packages: require-from-string: 2.0.2 uri-js: 4.4.1 + /ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + dev: true + /ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -5548,6 +5804,16 @@ packages: engines: {node: '>=8'} dev: true + /array.prototype.flat@1.3.1: + resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + es-shim-unscopables: 1.0.0 + dev: true + /arraybuffer.prototype.slice@1.0.1: resolution: {integrity: sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==} engines: {node: '>= 0.4'} @@ -5560,6 +5826,11 @@ packages: is-shared-array-buffer: 1.0.2 dev: true + /arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + dev: true + /asn1.js@5.4.1: resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} dependencies: @@ -5730,6 +6001,13 @@ packages: /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + /better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + dependencies: + is-windows: 1.0.2 + dev: true + /better-sqlite3@7.6.2: resolution: {integrity: sha512-S5zIU1Hink2AH4xPsN0W43T1/AJ5jrPh7Oy07ocuW/AKYYY02GWzz9NH0nbSMn/gw6fDZ5jZ1QsHt1BXAwJ6Lg==} requiresBuild: true @@ -5804,6 +6082,12 @@ packages: dependencies: fill-range: 7.0.1 + /breakword@1.0.6: + resolution: {integrity: sha512-yjxDAYyK/pBvws9H4xKYpLDpYKEH6CzrBPAuXq3x18I+c/2MkVtT3qAr7Oloi6Dss9qNhPVueAAVU1CSeNDIXw==} + dependencies: + wcwidth: 1.0.1 + dev: true + /brorand@1.1.0: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} dev: false @@ -5903,6 +6187,15 @@ packages: engines: {node: '>=6'} dev: true + /camelcase-keys@6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + map-obj: 4.3.0 + quick-lru: 4.0.1 + dev: true + /camelcase@5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} @@ -5970,6 +6263,10 @@ packages: engines: {node: '>=10'} dev: true + /chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + dev: true + /chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} dev: false @@ -5992,6 +6289,14 @@ packages: engines: {node: '>=6'} dev: true + /cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: true + /cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -6001,6 +6306,11 @@ packages: wrap-ansi: 7.0.0 dev: true + /clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + dev: true + /cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} @@ -6146,6 +6456,14 @@ packages: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true + /cross-spawn@5.1.0: + resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} + dependencies: + lru-cache: 4.1.5 + shebang-command: 1.2.0 + which: 1.3.1 + dev: true + /cross-spawn@6.0.5: resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} engines: {node: '>=4.8'} @@ -6170,6 +6488,32 @@ packages: resolution: {integrity: sha512-/RC5F4l1SCqD/jazwUF6+t34Cd8zTSAGZ7rvvZu1whZUhD2a5MOGKjSGowoGcpj/cbVZk1ZODIooJEQQq3nNAA==} dev: false + /csv-generate@3.4.3: + resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} + dev: true + + /csv-parse@4.16.3: + resolution: {integrity: sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==} + dev: true + + /csv-stringify@5.6.5: + resolution: {integrity: sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A==} + dev: true + + /csv@5.5.3: + resolution: {integrity: sha512-QTaY0XjjhTQOdguARF0lGKm5/mEq9PD9/VhZZegHDIBq2tQwgNpHc3dneD4mGo2iJs+fTKv5Bp0fZ+BRuY3Z0g==} + engines: {node: '>= 0.1.90'} + dependencies: + csv-generate: 3.4.3 + csv-parse: 4.16.3 + csv-stringify: 5.6.5 + stream-transform: 2.1.3 + dev: true + + /dataloader@1.4.0: + resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} + dev: true + /dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} dev: true @@ -6229,6 +6573,19 @@ packages: dependencies: ms: 2.1.2 + /decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + dev: true + + /decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dev: true + /decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -6253,6 +6610,12 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + /defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + dependencies: + clone: 1.0.4 + dev: true + /define-properties@1.2.0: resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} engines: {node: '>= 0.4'} @@ -6285,6 +6648,11 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + /detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + dev: true + /detect-libc@2.0.2: resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} engines: {node: '>=8'} @@ -6355,6 +6723,11 @@ packages: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} + /dotenv@8.6.0: + resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} + engines: {node: '>=10'} + dev: true + /ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} dependencies: @@ -6406,6 +6779,14 @@ packages: dependencies: once: 1.4.0 + /enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + dev: true + /entities@2.2.0: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} dev: false @@ -6479,6 +6860,12 @@ packages: has-tostringtag: 1.0.0 dev: true + /es-shim-unscopables@1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.3 + dev: true + /es-to-primitive@1.2.1: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} @@ -6996,6 +7383,19 @@ packages: transitivePeerDependencies: - supports-color + /extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + dev: true + + /external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + dev: true + /fast-copy@2.1.7: resolution: {integrity: sha512-ozrGwyuCTAy7YgFCua8rmqmytECYk/JYAMXcswOcm0qvGoE3tPb7ivBeIHTOK2DiapBhDZgacIhzhQIKU5TCfA==} dev: true @@ -7132,6 +7532,13 @@ packages: path-exists: 4.0.0 dev: true + /find-yarn-workspace-root2@1.2.16: + resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} + dependencies: + micromatch: 4.0.5 + pkg-dir: 4.2.0 + dev: true + /findit2@2.2.3: resolution: {integrity: sha512-lg/Moejf4qXovVutL0Lz4IsaPoNYMuxt4PA0nGqFxnJ1CTTGGlEO2wKgoDpwknhvZ8k4Q2F+eesgkLbG2Mxfog==} engines: {node: '>=0.8.22'} @@ -7193,6 +7600,24 @@ packages: universalify: 2.0.0 dev: true + /fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: true + + /fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: true + /fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -7382,6 +7807,11 @@ packages: uglify-js: 3.17.4 dev: false + /hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + dev: true + /has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} dev: true @@ -7525,6 +7955,10 @@ packages: - supports-color dev: true + /human-id@1.0.2: + resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} + dev: true + /human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -7686,6 +8120,13 @@ packages: engines: {node: '>= 0.4'} dev: true + /is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + dependencies: + ci-info: 3.8.0 + dev: true + /is-core-module@2.13.0: resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} dependencies: @@ -7739,6 +8180,11 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + /is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + dev: true + /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -7765,6 +8211,13 @@ packages: has-tostringtag: 1.0.0 dev: true + /is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + dependencies: + better-path-resolve: 1.0.0 + dev: true + /is-symbol@1.0.4: resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} engines: {node: '>= 0.4'} @@ -7785,6 +8238,11 @@ packages: call-bind: 1.0.2 dev: true + /is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + dev: true + /isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} dev: true @@ -8362,6 +8820,12 @@ packages: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} dev: true + /jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + /jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: @@ -8410,11 +8874,21 @@ packages: elliptic: 6.5.4 dev: false + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: true + /kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} dev: true + /kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + dev: true + /koalas@1.0.2: resolution: {integrity: sha512-RYhBbYaTTTHId3l6fnMZc3eGQNW6FVCqMG6AMwA5I1Mafr6AflaXeoi6x3xQuATRotGYRLk6+1ELZH4dstFNOA==} engines: {node: '>=0.10.0'} @@ -8460,6 +8934,16 @@ packages: strip-bom: 3.0.0 dev: true + /load-yaml-file@0.2.0: + resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} + engines: {node: '>=6'} + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + dev: true + /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -8530,6 +9014,10 @@ packages: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} dev: false + /lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + dev: true + /lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} dev: false @@ -8543,6 +9031,13 @@ packages: engines: {node: 14 || >=16.14} dev: false + /lru-cache@4.1.5: + resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + dependencies: + pseudomap: 1.0.2 + yallist: 2.1.2 + dev: true + /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: @@ -8601,6 +9096,16 @@ packages: tmpl: 1.0.5 dev: true + /map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + dev: true + + /map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + dev: true + /media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -8610,6 +9115,23 @@ packages: engines: {node: '>= 0.10.0'} dev: true + /meow@6.1.1: + resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==} + engines: {node: '>=8'} + dependencies: + '@types/minimist': 1.2.2 + camelcase-keys: 6.2.2 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 2.5.0 + read-pkg-up: 7.0.1 + redent: 3.0.0 + trim-newlines: 3.0.1 + type-fest: 0.13.1 + yargs-parser: 18.1.3 + dev: true + /merge-descriptors@1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} @@ -8657,6 +9179,11 @@ packages: engines: {node: '>=10'} dev: false + /min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + /minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} dev: false @@ -8677,6 +9204,15 @@ packages: dependencies: brace-expansion: 2.0.1 + /minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 + dev: true + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -8739,6 +9275,11 @@ packages: yallist: 4.0.0 dev: true + /mixme@0.5.9: + resolution: {integrity: sha512-VC5fg6ySUscaWUpI4gxCBTQMH2RdUpNrk+MsbpCYtIvf9SBJdiUey4qE7BXviJsJR4nDQxCZ+3yaYNW3guz/Pw==} + engines: {node: '>= 8.0.0'} + dev: true + /mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} dev: false @@ -8799,6 +9340,18 @@ packages: resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} dev: false + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: true + /node-gyp-build-optional-packages@5.0.3: resolution: {integrity: sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==} hasBin: true @@ -8978,6 +9531,22 @@ packages: type-check: 0.4.0 dev: true + /os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + dev: true + + /outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + dev: true + + /p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + dependencies: + p-map: 2.1.0 + dev: true + /p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} @@ -9009,6 +9578,11 @@ packages: p-limit: 3.1.0 dev: true + /p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + dev: true + /p-map@4.0.0: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} @@ -9189,6 +9763,11 @@ packages: engines: {node: '>=4'} dev: true + /pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + dev: true + /pify@5.0.0: resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} engines: {node: '>=10'} @@ -9306,6 +9885,16 @@ packages: tunnel-agent: 0.6.0 dev: false + /preferred-pm@3.0.3: + resolution: {integrity: sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==} + engines: {node: '>=10'} + dependencies: + find-up: 5.0.0 + find-yarn-workspace-root2: 1.2.16 + path-exists: 4.0.0 + which-pm: 2.0.0 + dev: true + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -9403,6 +9992,10 @@ packages: /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + /pseudomap@1.0.2: + resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + dev: true + /pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} dependencies: @@ -9429,6 +10022,11 @@ packages: /quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + /quick-lru@4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + dev: true + /range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -9460,6 +10058,15 @@ packages: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true + /read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + dev: true + /read-pkg@3.0.0: resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} engines: {node: '>=4'} @@ -9469,6 +10076,26 @@ packages: path-type: 3.0.0 dev: true + /read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + dependencies: + '@types/normalize-package-data': 2.4.1 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + dev: true + + /read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + dev: true + /readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -9498,6 +10125,14 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} + /redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + dev: true + /redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -9573,6 +10208,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + /require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + dev: true + /resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -9831,6 +10470,19 @@ packages: engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} dev: true + /smartwrap@2.0.2: + resolution: {integrity: sha512-vCsKNQxb7PnCNd2wY1WClWifAc2lwqsG8OaswpJkVJsvMGcnEntdTCDajZCkk93Ay1U3t/9puJmb525Rg5MZBA==} + engines: {node: '>=6'} + hasBin: true + dependencies: + array.prototype.flat: 1.3.1 + breakword: 1.0.6 + grapheme-splitter: 1.0.4 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + yargs: 15.4.1 + dev: true + /socks-proxy-agent@7.0.0: resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==} engines: {node: '>= 10'} @@ -9871,6 +10523,13 @@ packages: engines: {node: '>= 8'} dev: false + /spawndamnit@2.0.0: + resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} + dependencies: + cross-spawn: 5.1.0 + signal-exit: 3.0.7 + dev: true + /spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} dependencies: @@ -9936,6 +10595,12 @@ packages: readable-stream: 3.6.2 dev: false + /stream-transform@2.1.3: + resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==} + dependencies: + mixme: 0.5.9 + dev: true + /string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -10014,6 +10679,13 @@ packages: engines: {node: '>=6'} dev: true + /strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + dependencies: + min-indent: 1.0.1 + dev: true + /strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -10102,6 +10774,11 @@ packages: yallist: 4.0.0 dev: true + /term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + dev: true + /terminal-link@2.1.1: resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} engines: {node: '>=8'} @@ -10137,6 +10814,13 @@ packages: hasBin: true dev: false + /tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + dependencies: + os-tmpdir: 1.0.2 + dev: true + /tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} dev: true @@ -10164,6 +10848,15 @@ packages: ieee754: 1.2.1 dev: false + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: true + + /trim-newlines@3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + dev: true + /ts-morph@16.0.0: resolution: {integrity: sha512-jGNF0GVpFj0orFw55LTsQxVYEUOCWBAbR5Ls7fTYE5pQsbW18ssTb/6UXx/GYAEjS+DQTp8VoTw0vqYMiaaQuw==} dependencies: @@ -10256,6 +10949,20 @@ packages: typescript: 4.8.4 dev: true + /tty-table@4.2.1: + resolution: {integrity: sha512-xz0uKo+KakCQ+Dxj1D/tKn2FSyreSYWzdkL/BYhgN6oMW808g8QRMuh1atAV9fjTPbWBjfbkKQpI/5rEcnAc7g==} + engines: {node: '>=8.0.0'} + hasBin: true + dependencies: + chalk: 4.1.2 + csv: 5.5.3 + kleur: 4.1.5 + smartwrap: 2.0.2 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + yargs: 17.7.2 + dev: true + /tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} dependencies: @@ -10274,6 +10981,11 @@ packages: engines: {node: '>=4'} dev: true + /type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + dev: true + /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -10284,6 +10996,16 @@ packages: engines: {node: '>=10'} dev: true + /type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + dev: true + + /type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + dev: true + /type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} @@ -10410,6 +11132,11 @@ packages: imurmurhash: 0.1.4 dev: true + /universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: true + /universalify@2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} @@ -10481,6 +11208,23 @@ packages: makeerror: 1.0.12 dev: true + /wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + dependencies: + defaults: 1.0.4 + dev: true + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: true + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: true + /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: @@ -10491,6 +11235,18 @@ packages: is-symbol: 1.0.4 dev: true + /which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + dev: true + + /which-pm@2.0.0: + resolution: {integrity: sha512-Lhs9Pmyph0p5n5Z3mVnN0yWcbQYUAD7rbQUiMsQxOJ3T57k7RFe35SUwWMf7dsbDZks1uOmw4AecB/JMDj3v/w==} + engines: {node: '>=8.15'} + dependencies: + load-yaml-file: 0.2.0 + path-exists: 4.0.0 + dev: true + /which-typed-array@1.1.11: resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} engines: {node: '>= 0.4'} @@ -10527,6 +11283,15 @@ packages: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} dev: false + /wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -10564,11 +11329,19 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + /y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + dev: true + /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} dev: true + /yallist@2.1.2: + resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} + dev: true + /yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} dev: true @@ -10576,11 +11349,36 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + /yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: true + /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} dev: true + /yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + dev: true + /yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} From a7c42cfe3981e8f1491f8d7a09fde041a9e83afe Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 6 Sep 2023 19:27:50 -0500 Subject: [PATCH 220/237] Clean up prettier/eslint scripts (#1514) * add prettierignore, hoist script * upgrade prettier * bump prettier eslint deps * format all files * hoist prettier check * remove unused scripts, hoist lint:fix * remove npm-run-all, unused * hoist lint * remove lint scripts * improve lint scripts * remove prettierignores * downgrade prettier deps to fix codgen * reformat all files * update makefile, format * fix locklife * final format --------- Co-authored-by: dholms --- .eslintignore | 3 + .eslintrc | 8 +- .github/ISSUE_TEMPLATE/bug_report.md | 6 +- .github/ISSUE_TEMPLATE/feature_request.md | 1 - .github/workflows/publish.yaml | 6 +- .github/workflows/repo.yaml | 6 +- .prettierignore | 9 + CONTRIBUTORS.md | 2 +- Makefile | 2 +- SECURITY.md | 4 +- jest.config.base.js | 8 +- lexicons/app/bsky/actor/defs.json | 64 +- lexicons/app/bsky/actor/getPreferences.json | 3 +- lexicons/app/bsky/actor/getProfile.json | 7 +- lexicons/app/bsky/actor/getProfiles.json | 7 +- lexicons/app/bsky/actor/getSuggestions.json | 16 +- lexicons/app/bsky/actor/searchActors.json | 18 +- .../app/bsky/actor/searchActorsTypeahead.json | 14 +- lexicons/app/bsky/embed/external.json | 18 +- lexicons/app/bsky/embed/images.json | 20 +- lexicons/app/bsky/embed/record.json | 35 +- lexicons/app/bsky/embed/recordWithMedia.json | 10 +- lexicons/app/bsky/feed/defs.json | 105 ++-- .../app/bsky/feed/describeFeedGenerator.json | 12 +- lexicons/app/bsky/feed/generator.json | 18 +- lexicons/app/bsky/feed/getActorFeeds.json | 18 +- lexicons/app/bsky/feed/getActorLikes.json | 23 +- lexicons/app/bsky/feed/getAuthorFeed.json | 33 +- lexicons/app/bsky/feed/getFeed.json | 22 +- lexicons/app/bsky/feed/getFeedGenerator.json | 11 +- lexicons/app/bsky/feed/getFeedGenerators.json | 7 +- lexicons/app/bsky/feed/getFeedSkeleton.json | 22 +- lexicons/app/bsky/feed/getLikes.json | 27 +- lexicons/app/bsky/feed/getPostThread.json | 6 +- lexicons/app/bsky/feed/getPosts.json | 4 +- lexicons/app/bsky/feed/getRepostedBy.json | 24 +- lexicons/app/bsky/feed/getSuggestedFeeds.json | 16 +- lexicons/app/bsky/feed/getTimeline.json | 18 +- lexicons/app/bsky/feed/like.json | 4 +- lexicons/app/bsky/feed/post.json | 26 +- lexicons/app/bsky/feed/repost.json | 6 +- lexicons/app/bsky/graph/block.json | 4 +- lexicons/app/bsky/graph/defs.json | 46 +- lexicons/app/bsky/graph/follow.json | 4 +- lexicons/app/bsky/graph/getBlocks.json | 16 +- lexicons/app/bsky/graph/getFollowers.json | 23 +- lexicons/app/bsky/graph/getFollows.json | 23 +- lexicons/app/bsky/graph/getList.json | 20 +- lexicons/app/bsky/graph/getListMutes.json | 13 +- lexicons/app/bsky/graph/getLists.json | 15 +- lexicons/app/bsky/graph/getMutes.json | 16 +- lexicons/app/bsky/graph/list.json | 17 +- lexicons/app/bsky/graph/listitem.json | 6 +- .../app/bsky/notification/getUnreadCount.json | 4 +- .../bsky/notification/listNotifications.json | 50 +- .../app/bsky/notification/registerPush.json | 11 +- .../app/bsky/notification/updateSeen.json | 2 +- lexicons/app/bsky/richtext/facet.json | 12 +- lexicons/app/bsky/unspecced/getPopular.json | 18 +- .../unspecced/getPopularFeedGenerators.json | 18 +- .../bsky/unspecced/getTimelineSkeleton.json | 20 +- .../com/atproto/admin/disableInviteCodes.json | 8 +- .../com/atproto/admin/getInviteCodes.json | 21 +- .../atproto/admin/getModerationAction.json | 7 +- .../atproto/admin/getModerationActions.json | 21 +- .../atproto/admin/getModerationReport.json | 7 +- lexicons/com/atproto/admin/getRecord.json | 13 +- lexicons/com/atproto/admin/getRepo.json | 11 +- .../admin/resolveModerationReports.json | 6 +- .../admin/reverseModerationAction.json | 6 +- lexicons/com/atproto/admin/searchRepos.json | 23 +- .../com/atproto/admin/updateAccountEmail.json | 4 +- .../atproto/admin/updateAccountHandle.json | 6 +- .../com/atproto/identity/resolveHandle.json | 4 +- .../com/atproto/identity/updateHandle.json | 4 +- lexicons/com/atproto/label/queryLabels.json | 25 +- .../com/atproto/label/subscribeLabels.json | 11 +- .../com/atproto/moderation/createReport.json | 28 +- lexicons/com/atproto/moderation/defs.json | 76 +-- lexicons/com/atproto/repo/applyWrites.json | 26 +- lexicons/com/atproto/repo/createRecord.json | 8 +- lexicons/com/atproto/repo/deleteRecord.json | 4 +- lexicons/com/atproto/repo/describeRepo.json | 23 +- lexicons/com/atproto/repo/getRecord.json | 8 +- lexicons/com/atproto/repo/listRecords.json | 49 +- lexicons/com/atproto/repo/putRecord.json | 8 +- lexicons/com/atproto/repo/strongRef.json | 6 +- lexicons/com/atproto/repo/uploadBlob.json | 2 +- .../com/atproto/server/createAccount.json | 28 +- .../com/atproto/server/createAppPassword.json | 12 +- .../com/atproto/server/createInviteCode.json | 4 +- .../com/atproto/server/createInviteCodes.json | 16 +- .../com/atproto/server/createSession.json | 16 +- lexicons/com/atproto/server/defs.json | 28 +- .../com/atproto/server/deleteAccount.json | 4 +- .../com/atproto/server/deleteSession.json | 2 +- .../com/atproto/server/describeServer.json | 15 +- .../atproto/server/getAccountInviteCodes.json | 4 +- lexicons/com/atproto/server/getSession.json | 8 +- .../com/atproto/server/listAppPasswords.json | 10 +- .../com/atproto/server/refreshSession.json | 12 +- .../atproto/server/requestAccountDelete.json | 2 +- .../com/atproto/server/revokeAppPassword.json | 2 +- lexicons/com/atproto/sync/getBlob.json | 12 +- lexicons/com/atproto/sync/getBlocks.json | 6 +- lexicons/com/atproto/sync/getCheckout.json | 2 +- lexicons/com/atproto/sync/getHead.json | 6 +- .../com/atproto/sync/getLatestCommit.json | 8 +- lexicons/com/atproto/sync/getRecord.json | 6 +- lexicons/com/atproto/sync/listBlobs.json | 25 +- lexicons/com/atproto/sync/listRepos.json | 15 +- lexicons/com/atproto/sync/notifyOfUpdate.json | 7 +- lexicons/com/atproto/sync/requestCrawl.json | 7 +- lexicons/com/atproto/sync/subscribeRepos.json | 79 ++- package.json | 7 +- packages/README.md | 2 +- packages/api/README.md | 81 +-- packages/api/bench/agent.bench.ts | 4 +- packages/api/definitions/labels.json | 2 +- .../definitions/locale/en/label-groups.json | 2 +- .../locale/en/proposed-label-groups.json | 2 +- .../locale/en/proposed-labels.json | 2 +- .../post-moderation-behaviors.json | 546 ++++++++++-------- .../profile-moderation-behaviors.json | 305 ++++++---- packages/api/definitions/proposed-labels.json | 2 +- packages/api/docs/labels.md | 54 +- .../api/docs/moderation-behaviors/posts.md | 529 +---------------- .../api/docs/moderation-behaviors/profiles.md | 188 +----- packages/api/docs/moderation.md | 6 +- packages/api/tsconfig.build.json | 2 +- packages/aws/README.md | 2 +- packages/aws/package.json | 6 - packages/aws/tsconfig.build.json | 2 +- packages/aws/tsconfig.json | 4 +- packages/bsky/.eslintignore | 1 - packages/bsky/.prettierignore | 1 - packages/bsky/package.json | 6 - packages/common-web/package.json | 6 - packages/common-web/tsconfig.build.json | 2 +- packages/common-web/tsconfig.json | 4 +- packages/common/package.json | 6 - packages/common/tsconfig.build.json | 2 +- packages/common/tsconfig.json | 8 +- packages/crypto/README.md | 2 +- packages/crypto/jest.config.js | 2 +- packages/crypto/package.json | 6 - packages/crypto/tsconfig.build.json | 2 +- packages/crypto/tsconfig.json | 4 +- packages/dev-env/README.md | 2 +- packages/dev-env/package.json | 8 +- packages/dev-env/tsconfig.build.json | 2 +- packages/dev-env/tsconfig.json | 6 +- packages/identifier/package.json | 6 - packages/identifier/tsconfig.build.json | 2 +- packages/identifier/tsconfig.json | 8 +- packages/identity/README.md | 2 +- packages/identity/jest.config.js | 2 +- packages/identity/package.json | 6 - packages/identity/tsconfig.build.json | 2 +- packages/identity/tsconfig.json | 6 +- packages/lex-cli/README.md | 2 +- packages/lex-cli/package.json | 6 - packages/lex-cli/tsconfig.build.json | 2 +- packages/lex-cli/tsconfig.json | 6 +- packages/lexicon/package.json | 6 - packages/lexicon/tsconfig.build.json | 2 +- packages/lexicon/tsconfig.json | 2 +- packages/nsid/README.md | 12 +- packages/nsid/package.json | 6 - packages/nsid/tsconfig.build.json | 2 +- packages/nsid/tsconfig.json | 4 +- packages/pds/.eslintignore | 1 - packages/pds/.prettierignore | 2 - packages/pds/README.md | 2 +- packages/pds/package.json | 6 - .../src/mailer/templates/delete-account.hbs | 13 +- packages/pds/tsconfig.build.json | 2 +- packages/pds/tsconfig.json | 6 +- packages/repo/README.md | 2 +- packages/repo/package.json | 6 - packages/repo/tsconfig.build.json | 2 +- packages/repo/tsconfig.json | 6 +- packages/syntax/README.md | 20 +- packages/syntax/package.json | 6 - packages/syntax/tsconfig.build.json | 2 +- packages/syntax/tsconfig.json | 8 +- packages/uri/README.md | 10 +- packages/uri/package.json | 6 - packages/uri/tsconfig.build.json | 2 +- packages/uri/tsconfig.json | 6 +- packages/xrpc-server/README.md | 15 +- packages/xrpc-server/package.json | 6 - packages/xrpc-server/tsconfig.build.json | 2 +- packages/xrpc-server/tsconfig.json | 6 +- packages/xrpc/README.md | 29 +- packages/xrpc/package.json | 6 - packages/xrpc/tsconfig.build.json | 2 +- packages/xrpc/tsconfig.json | 4 +- pnpm-lock.yaml | 124 +--- pnpm-workspace.yaml | 4 +- services/bsky/indexer.js | 7 +- update-main-to-dist.js | 4 +- 202 files changed, 1816 insertions(+), 2161 deletions(-) create mode 100644 .eslintignore create mode 100644 .prettierignore delete mode 100644 packages/bsky/.eslintignore delete mode 100644 packages/bsky/.prettierignore delete mode 100644 packages/pds/.eslintignore delete mode 100644 packages/pds/.prettierignore diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000000..fc66834cbc0 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +packages/api/src/client +packages/bsky/src/lexicon +packages/pds/src/lexicon diff --git a/.eslintrc b/.eslintrc index 9d8f724f10a..8a278deb2c7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -13,7 +13,7 @@ "plugin:prettier/recommended", "prettier" ], - "ignorePatterns":[ + "ignorePatterns": [ "dist", "node_modules", "jest.config.base.js", @@ -26,7 +26,11 @@ "rules": { "no-var": "error", "prefer-const": "warn", - "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "no-misleading-character-class": "warn", + "@typescript-eslint/no-unused-vars": [ + "warn", + { "argsIgnorePattern": "^_" } + ], "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/explicit-module-boundary-types": "off", diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index f02d83fa7f7..e6f81a12d57 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,10 +4,10 @@ about: Create a report to help us improve title: '' labels: bug assignees: '' - --- **Describe the bug** + **To Reproduce** @@ -22,8 +22,8 @@ Steps to reproduce the behavior: **Details** - - Operating system: - - Node version: +- Operating system: +- Node version: **Additional context** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 0ac6b3c416b..0103c283eb2 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,7 +4,6 @@ about: Suggest an idea for this project title: '' labels: feature-request assignees: '' - --- **Is your feature request related to a problem? Please describe.** diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 4e93a6df1ba..ba7febd4b07 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -21,7 +21,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 18 - cache: "pnpm" + cache: 'pnpm' - run: pnpm i --frozen-lockfile - run: pnpm verify - name: Publish @@ -30,5 +30,5 @@ jobs: with: publish: pnpm release version: pnpm version-packages - commit: "Version packages" - title: "Version packages" + commit: 'Version packages' + title: 'Version packages' diff --git a/.github/workflows/repo.yaml b/.github/workflows/repo.yaml index 7fbe3b9f739..e387c442321 100644 --- a/.github/workflows/repo.yaml +++ b/.github/workflows/repo.yaml @@ -21,7 +21,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 18 - cache: "pnpm" + cache: 'pnpm' - run: pnpm i --frozen-lockfile - run: pnpm build test: @@ -38,7 +38,7 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 18 - cache: "pnpm" + cache: 'pnpm' - run: pnpm i --frozen-lockfile - run: pnpm test:withFlags --maxWorkers=1 --shard=${{ matrix.shard }} --passWithNoTests verify: @@ -52,6 +52,6 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 18 - cache: "pnpm" + cache: 'pnpm' - run: pnpm install --frozen-lockfile - run: pnpm verify diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000000..f45dcc122d0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +node_modules +dist +build +.nyc_output +coverage +pnpm-lock.yaml +.pnpm* +.changeset +*.d.ts diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 059698053c4..f10293dc002 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -14,4 +14,4 @@ ATProto receives so many contributions that we could never list everyone who des #### [TowhidKashem](https://github.com/TowhidKashem), Security disclosure, May 2023 -#### [DavidBuchanan314](https://github.com/DavidBuchanan314), Security disclosure, May 2023 \ No newline at end of file +#### [DavidBuchanan314](https://github.com/DavidBuchanan314), Security disclosure, May 2023 diff --git a/Makefile b/Makefile index 1ed13a81ff3..f8c36ce2bb0 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ lint: ## Run style checks and verify syntax .PHONY: fmt fmt: ## Run syntax re-formatting - pnpm prettier + pnpm format .PHONY: deps deps: ## Installs dependent libs using 'pnpm install' diff --git a/SECURITY.md b/SECURITY.md index d1602659ff1..0d6086fb204 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,8 +2,8 @@ ## Reporting a Vulnerability -Please do NOT report possible security vulnerabilities in public channels such as GitHub Issues. If you believe you have found a security vulnerability, please email us at `security@bsky.app` with a description of the issue. +Please do NOT report possible security vulnerabilities in public channels such as GitHub Issues. If you believe you have found a security vulnerability, please email us at `security@bsky.app` with a description of the issue. We will acknowledge the vulnerability as soon as possible - within 3 business days - and follow up when a fix lands. Please avoid discussing the vulnerability until we do so. -With your consent, we will add you to the repository [CONTRIBUTORS](https://github.com/bluesky-social/atproto/blob/main/CONTRIBUTORS.md) file. \ No newline at end of file +With your consent, we will add you to the repository [CONTRIBUTORS](https://github.com/bluesky-social/atproto/blob/main/CONTRIBUTORS.md) file. diff --git a/jest.config.base.js b/jest.config.base.js index 31a393e14f2..2b914a54e51 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -1,5 +1,5 @@ // Jest doesn't like ES modules, so we need to transpile them -// For each one, add them to this list, add them to +// For each one, add them to this list, add them to // "workspaces.nohoist" in the root package.json, and // make sure that a babel.config.js is in the package root const esModules = ['get-port', 'node-fetch'].join('|') @@ -8,12 +8,12 @@ const esModules = ['get-port', 'node-fetch'].join('|') module.exports = { roots: ['/src', '/tests'], transform: { - "^.+\\.(t|j)s?$": "@swc/jest", + '^.+\\.(t|j)s?$': '@swc/jest', }, transformIgnorePatterns: [`/node_modules/(?!${esModules})`], testRegex: '(/tests/.*.(test|spec)).(jsx?|tsx?)$', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - setupFiles: ["/../../test-setup.ts"], + setupFiles: ['/../../test-setup.ts'], verbose: true, - testTimeout: 60000 + testTimeout: 60000, } diff --git a/lexicons/app/bsky/actor/defs.json b/lexicons/app/bsky/actor/defs.json index cf2cafe06f0..bd321aa60ed 100644 --- a/lexicons/app/bsky/actor/defs.json +++ b/lexicons/app/bsky/actor/defs.json @@ -7,18 +7,18 @@ "type": "object", "required": ["did", "handle"], "properties": { - "did": {"type": "string", "format": "did"}, - "handle": {"type": "string", "format": "handle"}, + "did": { "type": "string", "format": "did" }, + "handle": { "type": "string", "format": "handle" }, "displayName": { "type": "string", "maxGraphemes": 64, "maxLength": 640 }, "avatar": { "type": "string" }, - "viewer": {"type": "ref", "ref": "#viewerState"}, + "viewer": { "type": "ref", "ref": "#viewerState" }, "labels": { "type": "array", - "items": {"type": "ref", "ref": "com.atproto.label.defs#label"} + "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } } } }, @@ -26,8 +26,8 @@ "type": "object", "required": ["did", "handle"], "properties": { - "did": {"type": "string", "format":"did"}, - "handle": {"type": "string", "format":"handle"}, + "did": { "type": "string", "format": "did" }, + "handle": { "type": "string", "format": "handle" }, "displayName": { "type": "string", "maxGraphemes": 64, @@ -39,11 +39,11 @@ "maxLength": 2560 }, "avatar": { "type": "string" }, - "indexedAt": {"type": "string", "format": "datetime"}, - "viewer": {"type": "ref", "ref": "#viewerState"}, + "indexedAt": { "type": "string", "format": "datetime" }, + "viewer": { "type": "ref", "ref": "#viewerState" }, "labels": { "type": "array", - "items": {"type": "ref", "ref": "com.atproto.label.defs#label"} + "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } } } }, @@ -51,8 +51,8 @@ "type": "object", "required": ["did", "handle"], "properties": { - "did": {"type": "string", "format": "did"}, - "handle": {"type": "string", "format": "handle"}, + "did": { "type": "string", "format": "did" }, + "handle": { "type": "string", "format": "handle" }, "displayName": { "type": "string", "maxGraphemes": 64, @@ -65,52 +65,54 @@ }, "avatar": { "type": "string" }, "banner": { "type": "string" }, - "followersCount": {"type": "integer"}, - "followsCount": {"type": "integer"}, - "postsCount": {"type": "integer"}, - "indexedAt": {"type": "string", "format": "datetime"}, - "viewer": {"type": "ref", "ref": "#viewerState"}, + "followersCount": { "type": "integer" }, + "followsCount": { "type": "integer" }, + "postsCount": { "type": "integer" }, + "indexedAt": { "type": "string", "format": "datetime" }, + "viewer": { "type": "ref", "ref": "#viewerState" }, "labels": { "type": "array", - "items": {"type": "ref", "ref": "com.atproto.label.defs#label"} + "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } } } }, "viewerState": { "type": "object", "properties": { - "muted": {"type": "boolean"}, - "mutedByList": {"type": "ref", "ref": "app.bsky.graph.defs#listViewBasic"}, - "blockedBy": {"type": "boolean"}, - "blocking": {"type": "string", "format": "at-uri"}, - "following": {"type": "string", "format": "at-uri"}, - "followedBy": {"type": "string", "format": "at-uri"} + "muted": { "type": "boolean" }, + "mutedByList": { + "type": "ref", + "ref": "app.bsky.graph.defs#listViewBasic" + }, + "blockedBy": { "type": "boolean" }, + "blocking": { "type": "string", "format": "at-uri" }, + "following": { "type": "string", "format": "at-uri" }, + "followedBy": { "type": "string", "format": "at-uri" } } }, "preferences": { "type": "array", "items": { "type": "union", - "refs": [ - "#adultContentPref", - "#contentLabelPref", - "#savedFeedsPref" - ] + "refs": ["#adultContentPref", "#contentLabelPref", "#savedFeedsPref"] } }, "adultContentPref": { "type": "object", "required": ["enabled"], "properties": { - "enabled": {"type": "boolean", "default": false} + "enabled": { "type": "boolean", "default": false } } }, "contentLabelPref": { "type": "object", "required": ["label", "visibility"], "properties": { - "label": {"type": "string"}, - "visibility": {"type": "string", "knownValues": ["show", "warn", "hide"]} + "label": { "type": "string" }, + "visibility": { + "type": "string", + "knownValues": ["show", "warn", "hide"] + } } }, "savedFeedsPref": { diff --git a/lexicons/app/bsky/actor/getPreferences.json b/lexicons/app/bsky/actor/getPreferences.json index c6285cffb53..cbd6b60bd6a 100644 --- a/lexicons/app/bsky/actor/getPreferences.json +++ b/lexicons/app/bsky/actor/getPreferences.json @@ -7,8 +7,7 @@ "description": "Get private preferences attached to the account.", "parameters": { "type": "params", - "properties": { - } + "properties": {} }, "output": { "encoding": "application/json", diff --git a/lexicons/app/bsky/actor/getProfile.json b/lexicons/app/bsky/actor/getProfile.json index d5f44b62284..d04ed0e159b 100644 --- a/lexicons/app/bsky/actor/getProfile.json +++ b/lexicons/app/bsky/actor/getProfile.json @@ -8,12 +8,15 @@ "type": "params", "required": ["actor"], "properties": { - "actor": {"type": "string", "format": "at-identifier"} + "actor": { "type": "string", "format": "at-identifier" } } }, "output": { "encoding": "application/json", - "schema": {"type": "ref", "ref": "app.bsky.actor.defs#profileViewDetailed"} + "schema": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileViewDetailed" + } } } } diff --git a/lexicons/app/bsky/actor/getProfiles.json b/lexicons/app/bsky/actor/getProfiles.json index cf4aac6fccf..ded213b6671 100644 --- a/lexicons/app/bsky/actor/getProfiles.json +++ b/lexicons/app/bsky/actor/getProfiles.json @@ -10,7 +10,7 @@ "properties": { "actors": { "type": "array", - "items": {"type": "string", "format": "at-identifier"}, + "items": { "type": "string", "format": "at-identifier" }, "maxLength": 25 } } @@ -23,7 +23,10 @@ "properties": { "profiles": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileViewDetailed"} + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileViewDetailed" + } } } } diff --git a/lexicons/app/bsky/actor/getSuggestions.json b/lexicons/app/bsky/actor/getSuggestions.json index de16ff7e2ac..38c30c2c9a6 100644 --- a/lexicons/app/bsky/actor/getSuggestions.json +++ b/lexicons/app/bsky/actor/getSuggestions.json @@ -8,8 +8,13 @@ "parameters": { "type": "params", "properties": { - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -18,10 +23,13 @@ "type": "object", "required": ["actors"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "actors": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileView" + } } } } diff --git a/lexicons/app/bsky/actor/searchActors.json b/lexicons/app/bsky/actor/searchActors.json index 7ad71530135..dc76ad8fc39 100644 --- a/lexicons/app/bsky/actor/searchActors.json +++ b/lexicons/app/bsky/actor/searchActors.json @@ -8,9 +8,14 @@ "parameters": { "type": "params", "properties": { - "term": {"type": "string"}, - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "term": { "type": "string" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -19,10 +24,13 @@ "type": "object", "required": ["actors"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "actors": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileView" + } } } } diff --git a/lexicons/app/bsky/actor/searchActorsTypeahead.json b/lexicons/app/bsky/actor/searchActorsTypeahead.json index 3402b230fda..7065f3d7117 100644 --- a/lexicons/app/bsky/actor/searchActorsTypeahead.json +++ b/lexicons/app/bsky/actor/searchActorsTypeahead.json @@ -8,8 +8,13 @@ "parameters": { "type": "params", "properties": { - "term": {"type": "string"}, - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50} + "term": { "type": "string" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + } } }, "output": { @@ -20,7 +25,10 @@ "properties": { "actors": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileViewBasic"} + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileViewBasic" + } } } } diff --git a/lexicons/app/bsky/embed/external.json b/lexicons/app/bsky/embed/external.json index 02b96b387a2..694787eb507 100644 --- a/lexicons/app/bsky/embed/external.json +++ b/lexicons/app/bsky/embed/external.json @@ -15,11 +15,11 @@ }, "external": { "type": "object", - "required": ["uri", "title", "description"], + "required": ["uri", "title", "description"], "properties": { - "uri": {"type": "string", "format": "uri"}, - "title": {"type": "string"}, - "description": {"type": "string"}, + "uri": { "type": "string", "format": "uri" }, + "title": { "type": "string" }, + "description": { "type": "string" }, "thumb": { "type": "blob", "accept": ["image/*"], @@ -39,12 +39,12 @@ }, "viewExternal": { "type": "object", - "required": ["uri", "title", "description"], + "required": ["uri", "title", "description"], "properties": { - "uri": {"type": "string", "format": "uri"}, - "title": {"type": "string"}, - "description": {"type": "string"}, - "thumb": {"type": "string"} + "uri": { "type": "string", "format": "uri" }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "thumb": { "type": "string" } } } } diff --git a/lexicons/app/bsky/embed/images.json b/lexicons/app/bsky/embed/images.json index 732b0d9a4e8..48975a60ae2 100644 --- a/lexicons/app/bsky/embed/images.json +++ b/lexicons/app/bsky/embed/images.json @@ -9,7 +9,7 @@ "properties": { "images": { "type": "array", - "items": {"type": "ref", "ref": "#image"}, + "items": { "type": "ref", "ref": "#image" }, "maxLength": 4 } } @@ -23,8 +23,8 @@ "accept": ["image/*"], "maxSize": 1000000 }, - "alt": {"type": "string"}, - "aspectRatio": {"type": "ref", "ref": "#aspectRatio"} + "alt": { "type": "string" }, + "aspectRatio": { "type": "ref", "ref": "#aspectRatio" } } }, "aspectRatio": { @@ -32,8 +32,8 @@ "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} + "width": { "type": "integer", "minimum": 1 }, + "height": { "type": "integer", "minimum": 1 } } }, "view": { @@ -42,7 +42,7 @@ "properties": { "images": { "type": "array", - "items": {"type": "ref", "ref": "#viewImage"}, + "items": { "type": "ref", "ref": "#viewImage" }, "maxLength": 4 } } @@ -51,10 +51,10 @@ "type": "object", "required": ["thumb", "fullsize", "alt"], "properties": { - "thumb": {"type": "string"}, - "fullsize": {"type": "string"}, - "alt": {"type": "string"}, - "aspectRatio": {"type": "ref", "ref": "#aspectRatio"} + "thumb": { "type": "string" }, + "fullsize": { "type": "string" }, + "alt": { "type": "string" }, + "aspectRatio": { "type": "ref", "ref": "#aspectRatio" } } } } diff --git a/lexicons/app/bsky/embed/record.json b/lexicons/app/bsky/embed/record.json index 3cd27d49503..0d7cb830ba8 100644 --- a/lexicons/app/bsky/embed/record.json +++ b/lexicons/app/bsky/embed/record.json @@ -7,12 +7,12 @@ "type": "object", "required": ["record"], "properties": { - "record": {"type": "ref", "ref": "com.atproto.repo.strongRef"} + "record": { "type": "ref", "ref": "com.atproto.repo.strongRef" } } }, "view": { "type": "object", - "required": ["record"], + "required": ["record"], "properties": { "record": { "type": "union", @@ -28,15 +28,18 @@ }, "viewRecord": { "type": "object", - "required": ["uri", "cid", "author", "value", "indexedAt"], + "required": ["uri", "cid", "author", "value", "indexedAt"], "properties": { - "uri": {"type": "string", "format": "at-uri"}, - "cid": {"type": "string", "format": "cid"}, - "author": {"type": "ref", "ref": "app.bsky.actor.defs#profileViewBasic"}, - "value": {"type": "unknown"}, + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "author": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileViewBasic" + }, + "value": { "type": "unknown" }, "labels": { "type": "array", - "items": {"type": "ref", "ref": "com.atproto.label.defs#label"} + "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } }, "embeds": { "type": "array", @@ -50,24 +53,24 @@ ] } }, - "indexedAt": {"type": "string", "format": "datetime"} + "indexedAt": { "type": "string", "format": "datetime" } } }, "viewNotFound": { "type": "object", - "required": ["uri", "notFound"], + "required": ["uri", "notFound"], "properties": { - "uri": {"type": "string", "format": "at-uri"}, - "notFound": {"type": "boolean", "const": true} + "uri": { "type": "string", "format": "at-uri" }, + "notFound": { "type": "boolean", "const": true } } }, "viewBlocked": { "type": "object", - "required": ["uri", "blocked", "author"], + "required": ["uri", "blocked", "author"], "properties": { - "uri": {"type": "string", "format": "at-uri"}, - "blocked": {"type": "boolean", "const": true}, - "author": {"type": "ref", "ref": "app.bsky.feed.defs#blockedAuthor"} + "uri": { "type": "string", "format": "at-uri" }, + "blocked": { "type": "boolean", "const": true }, + "author": { "type": "ref", "ref": "app.bsky.feed.defs#blockedAuthor" } } } } diff --git a/lexicons/app/bsky/embed/recordWithMedia.json b/lexicons/app/bsky/embed/recordWithMedia.json index 1e5346a8beb..296494d9ebc 100644 --- a/lexicons/app/bsky/embed/recordWithMedia.json +++ b/lexicons/app/bsky/embed/recordWithMedia.json @@ -13,10 +13,7 @@ }, "media": { "type": "union", - "refs": [ - "app.bsky.embed.images", - "app.bsky.embed.external" - ] + "refs": ["app.bsky.embed.images", "app.bsky.embed.external"] } } }, @@ -30,10 +27,7 @@ }, "media": { "type": "union", - "refs": [ - "app.bsky.embed.images#view", - "app.bsky.embed.external#view" - ] + "refs": ["app.bsky.embed.images#view", "app.bsky.embed.external#view"] } } } diff --git a/lexicons/app/bsky/feed/defs.json b/lexicons/app/bsky/feed/defs.json index 0dd61eadcba..7a9fcf5e68f 100644 --- a/lexicons/app/bsky/feed/defs.json +++ b/lexicons/app/bsky/feed/defs.json @@ -6,10 +6,13 @@ "type": "object", "required": ["uri", "cid", "author", "record", "indexedAt"], "properties": { - "uri": {"type": "string", "format": "at-uri"}, - "cid": {"type": "string", "format": "cid"}, - "author": {"type": "ref", "ref": "app.bsky.actor.defs#profileViewBasic"}, - "record": {"type": "unknown"}, + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "author": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileViewBasic" + }, + "record": { "type": "unknown" }, "embed": { "type": "union", "refs": [ @@ -19,58 +22,70 @@ "app.bsky.embed.recordWithMedia#view" ] }, - "replyCount": {"type": "integer"}, - "repostCount": {"type": "integer"}, - "likeCount": {"type": "integer"}, - "indexedAt": {"type": "string", "format": "datetime"}, - "viewer": {"type": "ref", "ref": "#viewerState"}, + "replyCount": { "type": "integer" }, + "repostCount": { "type": "integer" }, + "likeCount": { "type": "integer" }, + "indexedAt": { "type": "string", "format": "datetime" }, + "viewer": { "type": "ref", "ref": "#viewerState" }, "labels": { "type": "array", - "items": {"type": "ref", "ref": "com.atproto.label.defs#label"} + "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } } } }, "viewerState": { "type": "object", "properties": { - "repost": {"type": "string", "format": "at-uri"}, - "like": {"type": "string", "format": "at-uri"} + "repost": { "type": "string", "format": "at-uri" }, + "like": { "type": "string", "format": "at-uri" } } }, "feedViewPost": { "type": "object", "required": ["post"], "properties": { - "post": {"type": "ref", "ref": "#postView"}, - "reply": {"type": "ref", "ref": "#replyRef"}, - "reason": {"type": "union", "refs": ["#reasonRepost"]} + "post": { "type": "ref", "ref": "#postView" }, + "reply": { "type": "ref", "ref": "#replyRef" }, + "reason": { "type": "union", "refs": ["#reasonRepost"] } } }, "replyRef": { "type": "object", "required": ["root", "parent"], "properties": { - "root": {"type": "union", "refs": ["#postView", "#notFoundPost", "#blockedPost"]}, - "parent": {"type": "union", "refs": ["#postView", "#notFoundPost", "#blockedPost"]} + "root": { + "type": "union", + "refs": ["#postView", "#notFoundPost", "#blockedPost"] + }, + "parent": { + "type": "union", + "refs": ["#postView", "#notFoundPost", "#blockedPost"] + } } }, "reasonRepost": { "type": "object", "required": ["by", "indexedAt"], "properties": { - "by": {"type": "ref", "ref": "app.bsky.actor.defs#profileViewBasic"}, - "indexedAt": {"type": "string", "format": "datetime"} + "by": { "type": "ref", "ref": "app.bsky.actor.defs#profileViewBasic" }, + "indexedAt": { "type": "string", "format": "datetime" } } }, "threadViewPost": { "type": "object", "required": ["post"], "properties": { - "post": {"type": "ref", "ref": "#postView"}, - "parent": {"type": "union", "refs": ["#threadViewPost", "#notFoundPost", "#blockedPost"]}, + "post": { "type": "ref", "ref": "#postView" }, + "parent": { + "type": "union", + "refs": ["#threadViewPost", "#notFoundPost", "#blockedPost"] + }, "replies": { "type": "array", - "items": {"type": "union", "refs": ["#threadViewPost", "#notFoundPost", "#blockedPost"]} + "items": { + "type": "union", + "refs": ["#threadViewPost", "#notFoundPost", "#blockedPost"] + } } } }, @@ -78,24 +93,24 @@ "type": "object", "required": ["uri", "notFound"], "properties": { - "uri": {"type": "string", "format": "at-uri"}, - "notFound": {"type": "boolean", "const": true} + "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": "#blockedAuthor"} + "uri": { "type": "string", "format": "at-uri" }, + "blocked": { "type": "boolean", "const": true }, + "author": { "type": "ref", "ref": "#blockedAuthor" } } }, "blockedAuthor": { "type": "object", "required": ["did"], "properties": { - "did": {"type": "string", "format": "did"}, + "did": { "type": "string", "format": "did" }, "viewer": { "type": "ref", "ref": "app.bsky.actor.defs#viewerState" } } }, @@ -103,41 +118,45 @@ "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": "app.bsky.actor.defs#profileView"}, - "displayName": {"type": "string"}, - "description": {"type": "string", "maxGraphemes": 300, "maxLength": 3000}, + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "did": { "type": "string", "format": "did" }, + "creator": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" }, + "displayName": { "type": "string" }, + "description": { + "type": "string", + "maxGraphemes": 300, + "maxLength": 3000 + }, "descriptionFacets": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.richtext.facet"} + "items": { "type": "ref", "ref": "app.bsky.richtext.facet" } }, - "avatar": {"type": "string"}, - "likeCount": {"type": "integer", "minimum": 0 }, - "viewer": {"type": "ref", "ref": "#generatorViewerState"}, - "indexedAt": {"type": "string", "format": "datetime"} + "avatar": { "type": "string" }, + "likeCount": { "type": "integer", "minimum": 0 }, + "viewer": { "type": "ref", "ref": "#generatorViewerState" }, + "indexedAt": { "type": "string", "format": "datetime" } } }, "generatorViewerState": { "type": "object", "properties": { - "like": {"type": "string", "format": "at-uri"} + "like": { "type": "string", "format": "at-uri" } } }, "skeletonFeedPost": { "type": "object", "required": ["post"], "properties": { - "post": {"type": "string", "format": "at-uri"}, - "reason": {"type": "union", "refs": ["#skeletonReasonRepost"]} + "post": { "type": "string", "format": "at-uri" }, + "reason": { "type": "union", "refs": ["#skeletonReasonRepost"] } } }, "skeletonReasonRepost": { "type": "object", "required": ["repost"], "properties": { - "repost": {"type": "string", "format": "at-uri"} + "repost": { "type": "string", "format": "at-uri" } } } } diff --git a/lexicons/app/bsky/feed/describeFeedGenerator.json b/lexicons/app/bsky/feed/describeFeedGenerator.json index 265b72c8bb2..69c143e6016 100644 --- a/lexicons/app/bsky/feed/describeFeedGenerator.json +++ b/lexicons/app/bsky/feed/describeFeedGenerator.json @@ -11,12 +11,12 @@ "type": "object", "required": ["did", "feeds"], "properties": { - "did": {"type": "string", "format": "did"}, + "did": { "type": "string", "format": "did" }, "feeds": { "type": "array", - "items": {"type": "ref", "ref": "#feed"} + "items": { "type": "ref", "ref": "#feed" } }, - "links": {"type": "ref", "ref": "#links"} + "links": { "type": "ref", "ref": "#links" } } } } @@ -25,14 +25,14 @@ "type": "object", "required": ["uri"], "properties": { - "uri": {"type": "string", "format": "at-uri"} + "uri": { "type": "string", "format": "at-uri" } } }, "links": { "type": "object", "properties": { - "privacyPolicy": {"type": "string"}, - "termsOfService": {"type": "string"} + "privacyPolicy": { "type": "string" }, + "termsOfService": { "type": "string" } } } } diff --git a/lexicons/app/bsky/feed/generator.json b/lexicons/app/bsky/feed/generator.json index 78742b120f0..e31bb4484e1 100644 --- a/lexicons/app/bsky/feed/generator.json +++ b/lexicons/app/bsky/feed/generator.json @@ -10,12 +10,20 @@ "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}, + "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": "app.bsky.richtext.facet"} + "items": { "type": "ref", "ref": "app.bsky.richtext.facet" } }, "avatar": { "type": "blob", @@ -26,7 +34,7 @@ "type": "union", "refs": ["com.atproto.label.defs#selfLabels"] }, - "createdAt": {"type": "string", "format": "datetime"} + "createdAt": { "type": "string", "format": "datetime" } } } } diff --git a/lexicons/app/bsky/feed/getActorFeeds.json b/lexicons/app/bsky/feed/getActorFeeds.json index b47bd90e679..f34aece1609 100644 --- a/lexicons/app/bsky/feed/getActorFeeds.json +++ b/lexicons/app/bsky/feed/getActorFeeds.json @@ -9,9 +9,14 @@ "type": "params", "required": ["actor"], "properties": { - "actor": {"type": "string", "format": "at-identifier"}, - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "actor": { "type": "string", "format": "at-identifier" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -20,10 +25,13 @@ "type": "object", "required": ["feeds"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "feeds": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.feed.defs#generatorView"} + "items": { + "type": "ref", + "ref": "app.bsky.feed.defs#generatorView" + } } } } diff --git a/lexicons/app/bsky/feed/getActorLikes.json b/lexicons/app/bsky/feed/getActorLikes.json index 38bb353aaba..df5e45a4295 100644 --- a/lexicons/app/bsky/feed/getActorLikes.json +++ b/lexicons/app/bsky/feed/getActorLikes.json @@ -9,9 +9,14 @@ "type": "params", "required": ["actor"], "properties": { - "actor": {"type": "string", "format": "at-identifier"}, - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "actor": { "type": "string", "format": "at-identifier" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -20,18 +25,18 @@ "type": "object", "required": ["feed"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "feed": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.feed.defs#feedViewPost"} + "items": { + "type": "ref", + "ref": "app.bsky.feed.defs#feedViewPost" + } } } } }, - "errors": [ - {"name": "BlockedActor"}, - {"name": "BlockedByActor"} - ] + "errors": [{ "name": "BlockedActor" }, { "name": "BlockedByActor" }] } } } diff --git a/lexicons/app/bsky/feed/getAuthorFeed.json b/lexicons/app/bsky/feed/getAuthorFeed.json index fe7d4cae59e..c1edfb21330 100644 --- a/lexicons/app/bsky/feed/getAuthorFeed.json +++ b/lexicons/app/bsky/feed/getAuthorFeed.json @@ -9,10 +9,23 @@ "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"], "default": "posts_with_replies"} + "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" + ], + "default": "posts_with_replies" + } } }, "output": { @@ -21,18 +34,18 @@ "type": "object", "required": ["feed"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "feed": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.feed.defs#feedViewPost"} + "items": { + "type": "ref", + "ref": "app.bsky.feed.defs#feedViewPost" + } } } } }, - "errors": [ - {"name": "BlockedActor"}, - {"name": "BlockedByActor"} - ] + "errors": [{ "name": "BlockedActor" }, { "name": "BlockedByActor" }] } } } diff --git a/lexicons/app/bsky/feed/getFeed.json b/lexicons/app/bsky/feed/getFeed.json index 5ca96e11f55..9aaeb24c7db 100644 --- a/lexicons/app/bsky/feed/getFeed.json +++ b/lexicons/app/bsky/feed/getFeed.json @@ -9,9 +9,14 @@ "type": "params", "required": ["feed"], "properties": { - "feed": {"type": "string", "format": "at-uri"}, - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "feed": { "type": "string", "format": "at-uri" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -20,17 +25,18 @@ "type": "object", "required": ["feed"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "feed": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.feed.defs#feedViewPost"} + "items": { + "type": "ref", + "ref": "app.bsky.feed.defs#feedViewPost" + } } } } }, - "errors": [ - {"name": "UnknownFeed"} - ] + "errors": [{ "name": "UnknownFeed" }] } } } diff --git a/lexicons/app/bsky/feed/getFeedGenerator.json b/lexicons/app/bsky/feed/getFeedGenerator.json index 0bb588baeb0..5af09254f93 100644 --- a/lexicons/app/bsky/feed/getFeedGenerator.json +++ b/lexicons/app/bsky/feed/getFeedGenerator.json @@ -9,7 +9,7 @@ "type": "params", "required": ["feed"], "properties": { - "feed": {"type": "string", "format": "at-uri"} + "feed": { "type": "string", "format": "at-uri" } } }, "output": { @@ -18,9 +18,12 @@ "type": "object", "required": ["view", "isOnline", "isValid"], "properties": { - "view": {"type": "ref", "ref": "app.bsky.feed.defs#generatorView"}, - "isOnline": {"type": "boolean"}, - "isValid": {"type": "boolean"} + "view": { + "type": "ref", + "ref": "app.bsky.feed.defs#generatorView" + }, + "isOnline": { "type": "boolean" }, + "isValid": { "type": "boolean" } } } } diff --git a/lexicons/app/bsky/feed/getFeedGenerators.json b/lexicons/app/bsky/feed/getFeedGenerators.json index 595e4e96fcc..d0cae3b39d2 100644 --- a/lexicons/app/bsky/feed/getFeedGenerators.json +++ b/lexicons/app/bsky/feed/getFeedGenerators.json @@ -11,7 +11,7 @@ "properties": { "feeds": { "type": "array", - "items": {"type": "string", "format": "at-uri"} + "items": { "type": "string", "format": "at-uri" } } } }, @@ -23,7 +23,10 @@ "properties": { "feeds": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.feed.defs#generatorView"} + "items": { + "type": "ref", + "ref": "app.bsky.feed.defs#generatorView" + } } } } diff --git a/lexicons/app/bsky/feed/getFeedSkeleton.json b/lexicons/app/bsky/feed/getFeedSkeleton.json index 013e7bacb9c..d12b4bdc2b8 100644 --- a/lexicons/app/bsky/feed/getFeedSkeleton.json +++ b/lexicons/app/bsky/feed/getFeedSkeleton.json @@ -9,9 +9,14 @@ "type": "params", "required": ["feed"], "properties": { - "feed": {"type": "string", "format": "at-uri"}, - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "feed": { "type": "string", "format": "at-uri" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -20,17 +25,18 @@ "type": "object", "required": ["feed"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "feed": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.feed.defs#skeletonFeedPost"} + "items": { + "type": "ref", + "ref": "app.bsky.feed.defs#skeletonFeedPost" + } } } } }, - "errors": [ - {"name": "UnknownFeed"} - ] + "errors": [{ "name": "UnknownFeed" }] } } } diff --git a/lexicons/app/bsky/feed/getLikes.json b/lexicons/app/bsky/feed/getLikes.json index b10a75adaf1..e9b632684a8 100644 --- a/lexicons/app/bsky/feed/getLikes.json +++ b/lexicons/app/bsky/feed/getLikes.json @@ -8,10 +8,15 @@ "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"} + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -20,12 +25,12 @@ "type": "object", "required": ["uri", "likes"], "properties": { - "uri": {"type": "string", "format": "at-uri"}, - "cid": {"type": "string", "format": "cid"}, - "cursor": {"type": "string"}, + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "cursor": { "type": "string" }, "likes": { "type": "array", - "items": {"type": "ref", "ref": "#like"} + "items": { "type": "ref", "ref": "#like" } } } } @@ -35,9 +40,9 @@ "type": "object", "required": ["indexedAt", "createdAt", "actor"], "properties": { - "indexedAt": {"type": "string", "format": "datetime"}, - "createdAt": {"type": "string", "format": "datetime"}, - "actor": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} + "indexedAt": { "type": "string", "format": "datetime" }, + "createdAt": { "type": "string", "format": "datetime" }, + "actor": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" } } } } diff --git a/lexicons/app/bsky/feed/getPostThread.json b/lexicons/app/bsky/feed/getPostThread.json index f69a30c5f06..2d5c2e29a11 100644 --- a/lexicons/app/bsky/feed/getPostThread.json +++ b/lexicons/app/bsky/feed/getPostThread.json @@ -8,7 +8,7 @@ "type": "params", "required": ["uri"], "properties": { - "uri": {"type": "string", "format": "at-uri"}, + "uri": { "type": "string", "format": "at-uri" }, "depth": { "type": "integer", "default": 6, @@ -40,9 +40,7 @@ } } }, - "errors": [ - {"name": "NotFound"} - ] + "errors": [{ "name": "NotFound" }] } } } diff --git a/lexicons/app/bsky/feed/getPosts.json b/lexicons/app/bsky/feed/getPosts.json index 10d8d609a7c..37056417a27 100644 --- a/lexicons/app/bsky/feed/getPosts.json +++ b/lexicons/app/bsky/feed/getPosts.json @@ -11,7 +11,7 @@ "properties": { "uris": { "type": "array", - "items": {"type": "string", "format": "at-uri"}, + "items": { "type": "string", "format": "at-uri" }, "maxLength": 25 } } @@ -24,7 +24,7 @@ "properties": { "posts": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.feed.defs#postView"} + "items": { "type": "ref", "ref": "app.bsky.feed.defs#postView" } } } } diff --git a/lexicons/app/bsky/feed/getRepostedBy.json b/lexicons/app/bsky/feed/getRepostedBy.json index 4298b89666a..247ca305e67 100644 --- a/lexicons/app/bsky/feed/getRepostedBy.json +++ b/lexicons/app/bsky/feed/getRepostedBy.json @@ -8,10 +8,15 @@ "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"} + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -20,12 +25,15 @@ "type": "object", "required": ["uri", "repostedBy"], "properties": { - "uri": {"type": "string", "format": "at-uri"}, - "cid": {"type": "string", "format": "cid"}, - "cursor": {"type": "string"}, + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "cursor": { "type": "string" }, "repostedBy": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileView" + } } } } diff --git a/lexicons/app/bsky/feed/getSuggestedFeeds.json b/lexicons/app/bsky/feed/getSuggestedFeeds.json index db62ddd525f..de7c4fef753 100644 --- a/lexicons/app/bsky/feed/getSuggestedFeeds.json +++ b/lexicons/app/bsky/feed/getSuggestedFeeds.json @@ -8,8 +8,13 @@ "parameters": { "type": "params", "properties": { - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -18,10 +23,13 @@ "type": "object", "required": ["feeds"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "feeds": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.feed.defs#generatorView"} + "items": { + "type": "ref", + "ref": "app.bsky.feed.defs#generatorView" + } } } } diff --git a/lexicons/app/bsky/feed/getTimeline.json b/lexicons/app/bsky/feed/getTimeline.json index e5e13c496ec..49e1ae84b37 100644 --- a/lexicons/app/bsky/feed/getTimeline.json +++ b/lexicons/app/bsky/feed/getTimeline.json @@ -8,9 +8,14 @@ "parameters": { "type": "params", "properties": { - "algorithm": {"type": "string"}, - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "algorithm": { "type": "string" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -19,10 +24,13 @@ "type": "object", "required": ["feed"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "feed": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.feed.defs#feedViewPost"} + "items": { + "type": "ref", + "ref": "app.bsky.feed.defs#feedViewPost" + } } } } diff --git a/lexicons/app/bsky/feed/like.json b/lexicons/app/bsky/feed/like.json index ea0a7fa5f28..01d9a08a76c 100644 --- a/lexicons/app/bsky/feed/like.json +++ b/lexicons/app/bsky/feed/like.json @@ -9,8 +9,8 @@ "type": "object", "required": ["subject", "createdAt"], "properties": { - "subject": {"type": "ref", "ref": "com.atproto.repo.strongRef"}, - "createdAt": {"type": "string", "format": "datetime"} + "subject": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, + "createdAt": { "type": "string", "format": "datetime" } } } } diff --git a/lexicons/app/bsky/feed/post.json b/lexicons/app/bsky/feed/post.json index acefc29bf22..5622b5cfd50 100644 --- a/lexicons/app/bsky/feed/post.json +++ b/lexicons/app/bsky/feed/post.json @@ -9,17 +9,17 @@ "type": "object", "required": ["text", "createdAt"], "properties": { - "text": {"type": "string", "maxLength": 3000, "maxGraphemes": 300}, + "text": { "type": "string", "maxLength": 3000, "maxGraphemes": 300 }, "entities": { "type": "array", "description": "Deprecated: replaced by app.bsky.richtext.facet.", - "items": {"type": "ref", "ref": "#entity"} + "items": { "type": "ref", "ref": "#entity" } }, "facets": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.richtext.facet"} + "items": { "type": "ref", "ref": "app.bsky.richtext.facet" } }, - "reply": {"type": "ref", "ref": "#replyRef"}, + "reply": { "type": "ref", "ref": "#replyRef" }, "embed": { "type": "union", "refs": [ @@ -32,22 +32,22 @@ "langs": { "type": "array", "maxLength": 3, - "items": {"type": "string", "format": "language"} + "items": { "type": "string", "format": "language" } }, "labels": { "type": "union", "refs": ["com.atproto.label.defs#selfLabels"] }, - "createdAt": {"type": "string", "format": "datetime"} + "createdAt": { "type": "string", "format": "datetime" } } } }, - "replyRef":{ + "replyRef": { "type": "object", "required": ["root", "parent"], "properties": { - "root": {"type": "ref", "ref": "com.atproto.repo.strongRef"}, - "parent": {"type": "ref", "ref": "com.atproto.repo.strongRef"} + "root": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, + "parent": { "type": "ref", "ref": "com.atproto.repo.strongRef" } } }, "entity": { @@ -55,12 +55,12 @@ "description": "Deprecated: use facets instead.", "required": ["index", "type", "value"], "properties": { - "index": {"type": "ref", "ref": "#textSlice"}, + "index": { "type": "ref", "ref": "#textSlice" }, "type": { "type": "string", "description": "Expected values are 'mention' and 'link'." }, - "value": {"type": "string"} + "value": { "type": "string" } } }, "textSlice": { @@ -68,8 +68,8 @@ "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} + "start": { "type": "integer", "minimum": 0 }, + "end": { "type": "integer", "minimum": 0 } } } } diff --git a/lexicons/app/bsky/feed/repost.json b/lexicons/app/bsky/feed/repost.json index 752a032ecee..3b809a53a7e 100644 --- a/lexicons/app/bsky/feed/repost.json +++ b/lexicons/app/bsky/feed/repost.json @@ -9,10 +9,10 @@ "type": "object", "required": ["subject", "createdAt"], "properties": { - "subject": {"type": "ref", "ref": "com.atproto.repo.strongRef"}, - "createdAt": {"type": "string", "format": "datetime"} + "subject": { "type": "ref", "ref": "com.atproto.repo.strongRef" }, + "createdAt": { "type": "string", "format": "datetime" } } } } } -} \ No newline at end of file +} diff --git a/lexicons/app/bsky/graph/block.json b/lexicons/app/bsky/graph/block.json index 919a9c75e93..67821908d6a 100644 --- a/lexicons/app/bsky/graph/block.json +++ b/lexicons/app/bsky/graph/block.json @@ -10,8 +10,8 @@ "type": "object", "required": ["subject", "createdAt"], "properties": { - "subject": {"type": "string", "format": "did"}, - "createdAt": {"type": "string", "format": "datetime"} + "subject": { "type": "string", "format": "did" }, + "createdAt": { "type": "string", "format": "datetime" } } } } diff --git a/lexicons/app/bsky/graph/defs.json b/lexicons/app/bsky/graph/defs.json index d5de7e5422c..4366f9df368 100644 --- a/lexicons/app/bsky/graph/defs.json +++ b/lexicons/app/bsky/graph/defs.json @@ -6,46 +6,48 @@ "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": "#listPurpose"}, - "avatar": {"type": "string"}, - "viewer": {"type": "ref", "ref": "#listViewerState"}, - "indexedAt": {"type": "string", "format": "datetime"} + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "name": { "type": "string", "maxLength": 64, "minLength": 1 }, + "purpose": { "type": "ref", "ref": "#listPurpose" }, + "avatar": { "type": "string" }, + "viewer": { "type": "ref", "ref": "#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": "app.bsky.actor.defs#profileView"}, - "name": {"type": "string", "maxLength": 64, "minLength": 1}, - "purpose": {"type": "ref", "ref": "#listPurpose"}, - "description": {"type": "string", "maxGraphemes": 300, "maxLength": 3000}, + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "creator": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" }, + "name": { "type": "string", "maxLength": 64, "minLength": 1 }, + "purpose": { "type": "ref", "ref": "#listPurpose" }, + "description": { + "type": "string", + "maxGraphemes": 300, + "maxLength": 3000 + }, "descriptionFacets": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.richtext.facet"} + "items": { "type": "ref", "ref": "app.bsky.richtext.facet" } }, - "avatar": {"type": "string"}, - "viewer": {"type": "ref", "ref": "#listViewerState"}, - "indexedAt": {"type": "string", "format": "datetime"} + "avatar": { "type": "string" }, + "viewer": { "type": "ref", "ref": "#listViewerState" }, + "indexedAt": { "type": "string", "format": "datetime" } } }, "listItemView": { "type": "object", "required": ["subject"], "properties": { - "subject": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} + "subject": { "type": "ref", "ref": "app.bsky.actor.defs#profileView" } } }, "listPurpose": { "type": "string", - "knownValues": [ - "app.bsky.graph.defs#modlist" - ] + "knownValues": ["app.bsky.graph.defs#modlist"] }, "modlist": { "type": "token", @@ -54,7 +56,7 @@ "listViewerState": { "type": "object", "properties": { - "muted": {"type": "boolean"} + "muted": { "type": "boolean" } } } } diff --git a/lexicons/app/bsky/graph/follow.json b/lexicons/app/bsky/graph/follow.json index 5c04a3501c6..64783ae1d1b 100644 --- a/lexicons/app/bsky/graph/follow.json +++ b/lexicons/app/bsky/graph/follow.json @@ -10,8 +10,8 @@ "type": "object", "required": ["subject", "createdAt"], "properties": { - "subject": {"type": "string", "format": "did"}, - "createdAt": {"type": "string", "format": "datetime"} + "subject": { "type": "string", "format": "did" }, + "createdAt": { "type": "string", "format": "datetime" } } } } diff --git a/lexicons/app/bsky/graph/getBlocks.json b/lexicons/app/bsky/graph/getBlocks.json index 5b1d28f4ecd..1573e57faa3 100644 --- a/lexicons/app/bsky/graph/getBlocks.json +++ b/lexicons/app/bsky/graph/getBlocks.json @@ -8,8 +8,13 @@ "parameters": { "type": "params", "properties": { - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -18,10 +23,13 @@ "type": "object", "required": ["blocks"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "blocks": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileView" + } } } } diff --git a/lexicons/app/bsky/graph/getFollowers.json b/lexicons/app/bsky/graph/getFollowers.json index f9990de7cc2..f3824bbd699 100644 --- a/lexicons/app/bsky/graph/getFollowers.json +++ b/lexicons/app/bsky/graph/getFollowers.json @@ -9,9 +9,14 @@ "type": "params", "required": ["actor"], "properties": { - "actor": {"type": "string", "format": "at-identifier"}, - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "actor": { "type": "string", "format": "at-identifier" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -20,11 +25,17 @@ "type": "object", "required": ["subject", "followers"], "properties": { - "subject": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"}, - "cursor": {"type": "string"}, + "subject": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileView" + }, + "cursor": { "type": "string" }, "followers": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileView" + } } } } diff --git a/lexicons/app/bsky/graph/getFollows.json b/lexicons/app/bsky/graph/getFollows.json index 3a20b841976..04ce9fef8bf 100644 --- a/lexicons/app/bsky/graph/getFollows.json +++ b/lexicons/app/bsky/graph/getFollows.json @@ -9,9 +9,14 @@ "type": "params", "required": ["actor"], "properties": { - "actor": {"type": "string", "format": "at-identifier"}, - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "actor": { "type": "string", "format": "at-identifier" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -20,11 +25,17 @@ "type": "object", "required": ["subject", "follows"], "properties": { - "subject": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"}, - "cursor": {"type": "string"}, + "subject": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileView" + }, + "cursor": { "type": "string" }, "follows": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileView" + } } } } diff --git a/lexicons/app/bsky/graph/getList.json b/lexicons/app/bsky/graph/getList.json index 47b7bf9d49f..0241ee1e7f1 100644 --- a/lexicons/app/bsky/graph/getList.json +++ b/lexicons/app/bsky/graph/getList.json @@ -9,9 +9,14 @@ "type": "params", "required": ["list"], "properties": { - "list": {"type": "string", "format": "at-uri"}, - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "list": { "type": "string", "format": "at-uri" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -20,11 +25,14 @@ "type": "object", "required": ["list", "items"], "properties": { - "cursor": {"type": "string"}, - "list": {"type": "ref", "ref": "app.bsky.graph.defs#listView"}, + "cursor": { "type": "string" }, + "list": { "type": "ref", "ref": "app.bsky.graph.defs#listView" }, "items": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.graph.defs#listItemView"} + "items": { + "type": "ref", + "ref": "app.bsky.graph.defs#listItemView" + } } } } diff --git a/lexicons/app/bsky/graph/getListMutes.json b/lexicons/app/bsky/graph/getListMutes.json index f1ec812e501..44b3b652fde 100644 --- a/lexicons/app/bsky/graph/getListMutes.json +++ b/lexicons/app/bsky/graph/getListMutes.json @@ -8,8 +8,13 @@ "parameters": { "type": "params", "properties": { - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -18,10 +23,10 @@ "type": "object", "required": ["lists"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "lists": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.graph.defs#listView"} + "items": { "type": "ref", "ref": "app.bsky.graph.defs#listView" } } } } diff --git a/lexicons/app/bsky/graph/getLists.json b/lexicons/app/bsky/graph/getLists.json index 09c6bcc9f95..de83a50d3eb 100644 --- a/lexicons/app/bsky/graph/getLists.json +++ b/lexicons/app/bsky/graph/getLists.json @@ -9,9 +9,14 @@ "type": "params", "required": ["actor"], "properties": { - "actor": {"type": "string", "format": "at-identifier"}, - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "actor": { "type": "string", "format": "at-identifier" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -20,10 +25,10 @@ "type": "object", "required": ["lists"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "lists": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.graph.defs#listView"} + "items": { "type": "ref", "ref": "app.bsky.graph.defs#listView" } } } } diff --git a/lexicons/app/bsky/graph/getMutes.json b/lexicons/app/bsky/graph/getMutes.json index 150d6858271..aba63ea3043 100644 --- a/lexicons/app/bsky/graph/getMutes.json +++ b/lexicons/app/bsky/graph/getMutes.json @@ -8,8 +8,13 @@ "parameters": { "type": "params", "properties": { - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -18,10 +23,13 @@ "type": "object", "required": ["mutes"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "mutes": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"} + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileView" + } } } } diff --git a/lexicons/app/bsky/graph/list.json b/lexicons/app/bsky/graph/list.json index c3583b55e14..ccc845a6926 100644 --- a/lexicons/app/bsky/graph/list.json +++ b/lexicons/app/bsky/graph/list.json @@ -10,12 +10,19 @@ "type": "object", "required": ["name", "purpose", "createdAt"], "properties": { - "purpose": {"type": "ref", "ref": "app.bsky.graph.defs#listPurpose"}, - "name": {"type": "string", "maxLength": 64, "minLength": 1}, - "description": {"type": "string", "maxGraphemes": 300, "maxLength": 3000}, + "purpose": { + "type": "ref", + "ref": "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": "app.bsky.richtext.facet"} + "items": { "type": "ref", "ref": "app.bsky.richtext.facet" } }, "avatar": { "type": "blob", @@ -26,7 +33,7 @@ "type": "union", "refs": ["com.atproto.label.defs#selfLabels"] }, - "createdAt": {"type": "string", "format": "datetime"} + "createdAt": { "type": "string", "format": "datetime" } } } } diff --git a/lexicons/app/bsky/graph/listitem.json b/lexicons/app/bsky/graph/listitem.json index c8f82e7fdfc..f05a1641e6f 100644 --- a/lexicons/app/bsky/graph/listitem.json +++ b/lexicons/app/bsky/graph/listitem.json @@ -10,9 +10,9 @@ "type": "object", "required": ["subject", "list", "createdAt"], "properties": { - "subject": {"type": "string", "format": "did"}, - "list": {"type": "string", "format": "at-uri"}, - "createdAt": {"type": "string", "format": "datetime"} + "subject": { "type": "string", "format": "did" }, + "list": { "type": "string", "format": "at-uri" }, + "createdAt": { "type": "string", "format": "datetime" } } } } diff --git a/lexicons/app/bsky/notification/getUnreadCount.json b/lexicons/app/bsky/notification/getUnreadCount.json index ba213622872..8b0cdc3af46 100644 --- a/lexicons/app/bsky/notification/getUnreadCount.json +++ b/lexicons/app/bsky/notification/getUnreadCount.json @@ -7,7 +7,7 @@ "parameters": { "type": "params", "properties": { - "seenAt": { "type": "string", "format": "datetime"} + "seenAt": { "type": "string", "format": "datetime" } } }, "output": { @@ -16,7 +16,7 @@ "type": "object", "required": ["count"], "properties": { - "count": { "type": "integer"} + "count": { "type": "integer" } } } } diff --git a/lexicons/app/bsky/notification/listNotifications.json b/lexicons/app/bsky/notification/listNotifications.json index d3775e6177c..c5a9aee0fd4 100644 --- a/lexicons/app/bsky/notification/listNotifications.json +++ b/lexicons/app/bsky/notification/listNotifications.json @@ -7,9 +7,14 @@ "parameters": { "type": "params", "properties": { - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"}, - "seenAt": { "type": "string", "format": "datetime"} + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" }, + "seenAt": { "type": "string", "format": "datetime" } } }, "output": { @@ -18,10 +23,10 @@ "type": "object", "required": ["notifications"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "notifications": { "type": "array", - "items": {"type": "ref", "ref": "#notification"} + "items": { "type": "ref", "ref": "#notification" } } } } @@ -29,23 +34,38 @@ }, "notification": { "type": "object", - "required": ["uri", "cid", "author", "reason", "record", "isRead", "indexedAt"], + "required": [ + "uri", + "cid", + "author", + "reason", + "record", + "isRead", + "indexedAt" + ], "properties": { - "uri": {"type": "string", "format": "at-uri"}, - "cid": {"type": "string", "format": "cid" }, - "author": {"type": "ref", "ref": "app.bsky.actor.defs#profileView"}, + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "author": { "type": "ref", "ref": "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"] + "knownValues": [ + "like", + "repost", + "follow", + "mention", + "reply", + "quote" + ] }, - "reasonSubject": {"type": "string", "format": "at-uri"}, - "record": {"type": "unknown"}, - "isRead": {"type": "boolean"}, - "indexedAt": {"type": "string", "format": "datetime"}, + "reasonSubject": { "type": "string", "format": "at-uri" }, + "record": { "type": "unknown" }, + "isRead": { "type": "boolean" }, + "indexedAt": { "type": "string", "format": "datetime" }, "labels": { "type": "array", - "items": {"type": "ref", "ref": "com.atproto.label.defs#label"} + "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } } } } diff --git a/lexicons/app/bsky/notification/registerPush.json b/lexicons/app/bsky/notification/registerPush.json index af83cfaf89c..159dd370049 100644 --- a/lexicons/app/bsky/notification/registerPush.json +++ b/lexicons/app/bsky/notification/registerPush.json @@ -11,10 +11,13 @@ "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"} + "serviceDid": { "type": "string", "format": "did" }, + "token": { "type": "string" }, + "platform": { + "type": "string", + "knownValues": ["ios", "android", "web"] + }, + "appId": { "type": "string" } } } } diff --git a/lexicons/app/bsky/notification/updateSeen.json b/lexicons/app/bsky/notification/updateSeen.json index 5f54d9ae110..33626343e51 100644 --- a/lexicons/app/bsky/notification/updateSeen.json +++ b/lexicons/app/bsky/notification/updateSeen.json @@ -11,7 +11,7 @@ "type": "object", "required": ["seenAt"], "properties": { - "seenAt": { "type": "string", "format": "datetime"} + "seenAt": { "type": "string", "format": "datetime" } } } } diff --git a/lexicons/app/bsky/richtext/facet.json b/lexicons/app/bsky/richtext/facet.json index 714fe8151d3..9addf2f34b7 100644 --- a/lexicons/app/bsky/richtext/facet.json +++ b/lexicons/app/bsky/richtext/facet.json @@ -6,10 +6,10 @@ "type": "object", "required": ["index", "features"], "properties": { - "index": {"type": "ref", "ref": "#byteSlice"}, + "index": { "type": "ref", "ref": "#byteSlice" }, "features": { "type": "array", - "items": {"type": "union", "refs": ["#mention", "#link"]} + "items": { "type": "union", "refs": ["#mention", "#link"] } } } }, @@ -18,7 +18,7 @@ "description": "A facet feature for actor mentions.", "required": ["did"], "properties": { - "did": {"type": "string", "format": "did"} + "did": { "type": "string", "format": "did" } } }, "link": { @@ -26,7 +26,7 @@ "description": "A facet feature for links.", "required": ["uri"], "properties": { - "uri": {"type": "string", "format": "uri"} + "uri": { "type": "string", "format": "uri" } } }, "byteSlice": { @@ -34,8 +34,8 @@ "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} + "byteStart": { "type": "integer", "minimum": 0 }, + "byteEnd": { "type": "integer", "minimum": 0 } } } } diff --git a/lexicons/app/bsky/unspecced/getPopular.json b/lexicons/app/bsky/unspecced/getPopular.json index c6fe559c56c..791968c5ef9 100644 --- a/lexicons/app/bsky/unspecced/getPopular.json +++ b/lexicons/app/bsky/unspecced/getPopular.json @@ -8,9 +8,14 @@ "parameters": { "type": "params", "properties": { - "includeNsfw": {"type": "boolean", "default": false}, - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "includeNsfw": { "type": "boolean", "default": false }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -19,10 +24,13 @@ "type": "object", "required": ["feed"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "feed": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.feed.defs#feedViewPost"} + "items": { + "type": "ref", + "ref": "app.bsky.feed.defs#feedViewPost" + } } } } diff --git a/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json b/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json index 55a85bcb51d..ddeb7e7fab2 100644 --- a/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json +++ b/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json @@ -8,9 +8,14 @@ "parameters": { "type": "params", "properties": { - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"}, - "query": {"type": "string"} + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" }, + "query": { "type": "string" } } }, "output": { @@ -19,10 +24,13 @@ "type": "object", "required": ["feeds"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "feeds": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.feed.defs#generatorView"} + "items": { + "type": "ref", + "ref": "app.bsky.feed.defs#generatorView" + } } } } diff --git a/lexicons/app/bsky/unspecced/getTimelineSkeleton.json b/lexicons/app/bsky/unspecced/getTimelineSkeleton.json index 7ac6d7dbdec..c720b81832d 100644 --- a/lexicons/app/bsky/unspecced/getTimelineSkeleton.json +++ b/lexicons/app/bsky/unspecced/getTimelineSkeleton.json @@ -8,8 +8,13 @@ "parameters": { "type": "params", "properties": { - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -18,17 +23,18 @@ "type": "object", "required": ["feed"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "feed": { "type": "array", - "items": {"type": "ref", "ref": "app.bsky.feed.defs#skeletonFeedPost"} + "items": { + "type": "ref", + "ref": "app.bsky.feed.defs#skeletonFeedPost" + } } } } }, - "errors": [ - {"name": "UnknownFeed"} - ] + "errors": [{ "name": "UnknownFeed" }] } } } diff --git a/lexicons/com/atproto/admin/disableInviteCodes.json b/lexicons/com/atproto/admin/disableInviteCodes.json index bfab5479ac6..f84bc05f203 100644 --- a/lexicons/com/atproto/admin/disableInviteCodes.json +++ b/lexicons/com/atproto/admin/disableInviteCodes.json @@ -11,12 +11,12 @@ "type": "object", "properties": { "codes": { - "type": "array", - "items": {"type": "string"} + "type": "array", + "items": { "type": "string" } }, "accounts": { - "type": "array", - "items": {"type": "string"} + "type": "array", + "items": { "type": "string" } } } } diff --git a/lexicons/com/atproto/admin/getInviteCodes.json b/lexicons/com/atproto/admin/getInviteCodes.json index c74a6d09bab..895f1259d7c 100644 --- a/lexicons/com/atproto/admin/getInviteCodes.json +++ b/lexicons/com/atproto/admin/getInviteCodes.json @@ -10,14 +10,16 @@ "properties": { "sort": { "type": "string", - "knownValues": [ - "recent", - "usage" - ], + "knownValues": ["recent", "usage"], "default": "recent" }, - "limit": {"type": "integer", "minimum": 1, "maximum": 500, "default": 100}, - "cursor": {"type": "string"} + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 500, + "default": 100 + }, + "cursor": { "type": "string" } } }, "output": { @@ -26,10 +28,13 @@ "type": "object", "required": ["codes"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "codes": { "type": "array", - "items": {"type": "ref", "ref": "com.atproto.server.defs#inviteCode"} + "items": { + "type": "ref", + "ref": "com.atproto.server.defs#inviteCode" + } } } } diff --git a/lexicons/com/atproto/admin/getModerationAction.json b/lexicons/com/atproto/admin/getModerationAction.json index b733ba670a8..b25ccc227e1 100644 --- a/lexicons/com/atproto/admin/getModerationAction.json +++ b/lexicons/com/atproto/admin/getModerationAction.json @@ -9,12 +9,15 @@ "type": "params", "required": ["id"], "properties": { - "id": {"type": "integer"} + "id": { "type": "integer" } } }, "output": { "encoding": "application/json", - "schema": {"type": "ref", "ref": "com.atproto.admin.defs#actionViewDetail"} + "schema": { + "type": "ref", + "ref": "com.atproto.admin.defs#actionViewDetail" + } } } } diff --git a/lexicons/com/atproto/admin/getModerationActions.json b/lexicons/com/atproto/admin/getModerationActions.json index 20ac9144a00..89e16fd6919 100644 --- a/lexicons/com/atproto/admin/getModerationActions.json +++ b/lexicons/com/atproto/admin/getModerationActions.json @@ -8,9 +8,14 @@ "parameters": { "type": "params", "properties": { - "subject": {"type": "string"}, - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "subject": { "type": "string" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -19,8 +24,14 @@ "type": "object", "required": ["actions"], "properties": { - "cursor": {"type": "string"}, - "actions": {"type": "array", "items": {"type": "ref", "ref": "com.atproto.admin.defs#actionView"}} + "cursor": { "type": "string" }, + "actions": { + "type": "array", + "items": { + "type": "ref", + "ref": "com.atproto.admin.defs#actionView" + } + } } } } diff --git a/lexicons/com/atproto/admin/getModerationReport.json b/lexicons/com/atproto/admin/getModerationReport.json index 2f1eed8d920..3e84e13e676 100644 --- a/lexicons/com/atproto/admin/getModerationReport.json +++ b/lexicons/com/atproto/admin/getModerationReport.json @@ -9,12 +9,15 @@ "type": "params", "required": ["id"], "properties": { - "id": {"type": "integer"} + "id": { "type": "integer" } } }, "output": { "encoding": "application/json", - "schema": {"type": "ref", "ref": "com.atproto.admin.defs#reportViewDetail"} + "schema": { + "type": "ref", + "ref": "com.atproto.admin.defs#reportViewDetail" + } } } } diff --git a/lexicons/com/atproto/admin/getRecord.json b/lexicons/com/atproto/admin/getRecord.json index b0790187b1e..a16fe484feb 100644 --- a/lexicons/com/atproto/admin/getRecord.json +++ b/lexicons/com/atproto/admin/getRecord.json @@ -9,17 +9,18 @@ "type": "params", "required": ["uri"], "properties": { - "uri": {"type": "string", "format": "at-uri"}, - "cid": {"type": "string", "format": "cid"} + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" } } }, "output": { "encoding": "application/json", - "schema": {"type": "ref", "ref": "com.atproto.admin.defs#recordViewDetail"} + "schema": { + "type": "ref", + "ref": "com.atproto.admin.defs#recordViewDetail" + } }, - "errors": [ - {"name": "RecordNotFound"} - ] + "errors": [{ "name": "RecordNotFound" }] } } } diff --git a/lexicons/com/atproto/admin/getRepo.json b/lexicons/com/atproto/admin/getRepo.json index 972c00ced89..d8cfd883f72 100644 --- a/lexicons/com/atproto/admin/getRepo.json +++ b/lexicons/com/atproto/admin/getRepo.json @@ -9,16 +9,17 @@ "type": "params", "required": ["did"], "properties": { - "did": {"type": "string", "format":"did"} + "did": { "type": "string", "format": "did" } } }, "output": { "encoding": "application/json", - "schema": {"type": "ref", "ref": "com.atproto.admin.defs#repoViewDetail"} + "schema": { + "type": "ref", + "ref": "com.atproto.admin.defs#repoViewDetail" + } }, - "errors": [ - {"name": "RepoNotFound"} - ] + "errors": [{ "name": "RepoNotFound" }] } } } diff --git a/lexicons/com/atproto/admin/resolveModerationReports.json b/lexicons/com/atproto/admin/resolveModerationReports.json index 536379e195e..0cc5c1df2a2 100644 --- a/lexicons/com/atproto/admin/resolveModerationReports.json +++ b/lexicons/com/atproto/admin/resolveModerationReports.json @@ -11,9 +11,9 @@ "type": "object", "required": ["actionId", "reportIds", "createdBy"], "properties": { - "actionId": {"type": "integer"}, - "reportIds": {"type": "array", "items": {"type": "integer"}}, - "createdBy": {"type": "string", "format": "did"} + "actionId": { "type": "integer" }, + "reportIds": { "type": "array", "items": { "type": "integer" } }, + "createdBy": { "type": "string", "format": "did" } } } }, diff --git a/lexicons/com/atproto/admin/reverseModerationAction.json b/lexicons/com/atproto/admin/reverseModerationAction.json index 496be245c30..9b479dcc8e1 100644 --- a/lexicons/com/atproto/admin/reverseModerationAction.json +++ b/lexicons/com/atproto/admin/reverseModerationAction.json @@ -11,9 +11,9 @@ "type": "object", "required": ["id", "reason", "createdBy"], "properties": { - "id": {"type": "integer"}, - "reason": {"type": "string"}, - "createdBy": {"type": "string", "format": "did"} + "id": { "type": "integer" }, + "reason": { "type": "string" }, + "createdBy": { "type": "string", "format": "did" } } } }, diff --git a/lexicons/com/atproto/admin/searchRepos.json b/lexicons/com/atproto/admin/searchRepos.json index 8955866ff5f..fb9c90f343c 100644 --- a/lexicons/com/atproto/admin/searchRepos.json +++ b/lexicons/com/atproto/admin/searchRepos.json @@ -8,10 +8,15 @@ "parameters": { "type": "params", "properties": { - "term": {"type": "string"}, - "invitedBy": {"type": "string"}, - "limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 50}, - "cursor": {"type": "string"} + "term": { "type": "string" }, + "invitedBy": { "type": "string" }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { "type": "string" } } }, "output": { @@ -20,8 +25,14 @@ "type": "object", "required": ["repos"], "properties": { - "cursor": {"type": "string"}, - "repos": {"type": "array", "items": {"type": "ref", "ref": "com.atproto.admin.defs#repoView"}} + "cursor": { "type": "string" }, + "repos": { + "type": "array", + "items": { + "type": "ref", + "ref": "com.atproto.admin.defs#repoView" + } + } } } } diff --git a/lexicons/com/atproto/admin/updateAccountEmail.json b/lexicons/com/atproto/admin/updateAccountEmail.json index 98a66e843d1..a4cf711dee9 100644 --- a/lexicons/com/atproto/admin/updateAccountEmail.json +++ b/lexicons/com/atproto/admin/updateAccountEmail.json @@ -16,10 +16,10 @@ "format": "at-identifier", "description": "The handle or DID of the repo." }, - "email": {"type": "string"} + "email": { "type": "string" } } } } } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/admin/updateAccountHandle.json b/lexicons/com/atproto/admin/updateAccountHandle.json index c9809b97af2..15442e98beb 100644 --- a/lexicons/com/atproto/admin/updateAccountHandle.json +++ b/lexicons/com/atproto/admin/updateAccountHandle.json @@ -11,11 +11,11 @@ "type": "object", "required": ["did", "handle"], "properties": { - "did": {"type": "string", "format": "did"}, - "handle": {"type": "string", "format": "handle"} + "did": { "type": "string", "format": "did" }, + "handle": { "type": "string", "format": "handle" } } } } } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/identity/resolveHandle.json b/lexicons/com/atproto/identity/resolveHandle.json index 5c22f78d6b3..ae5aab8f8fc 100644 --- a/lexicons/com/atproto/identity/resolveHandle.json +++ b/lexicons/com/atproto/identity/resolveHandle.json @@ -22,10 +22,10 @@ "type": "object", "required": ["did"], "properties": { - "did": {"type": "string", "format": "did"} + "did": { "type": "string", "format": "did" } } } } } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/identity/updateHandle.json b/lexicons/com/atproto/identity/updateHandle.json index 53789d539cb..2b595d189f5 100644 --- a/lexicons/com/atproto/identity/updateHandle.json +++ b/lexicons/com/atproto/identity/updateHandle.json @@ -11,10 +11,10 @@ "type": "object", "required": ["handle"], "properties": { - "handle": {"type": "string", "format": "handle"} + "handle": { "type": "string", "format": "handle" } } } } } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/label/queryLabels.json b/lexicons/com/atproto/label/queryLabels.json index c51597ceb8d..f4773f255e3 100644 --- a/lexicons/com/atproto/label/queryLabels.json +++ b/lexicons/com/atproto/label/queryLabels.json @@ -10,17 +10,22 @@ "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" + "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" + "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 }, - "limit": {"type": "integer", "minimum": 1, "maximum": 250, "default": 50}, - "cursor": {"type": "string"} + "cursor": { "type": "string" } } }, "output": { @@ -29,10 +34,10 @@ "type": "object", "required": ["labels"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "labels": { "type": "array", - "items": {"type": "ref", "ref": "com.atproto.label.defs#label"} + "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } } } } diff --git a/lexicons/com/atproto/label/subscribeLabels.json b/lexicons/com/atproto/label/subscribeLabels.json index 65a9e1a4a3c..044036cfad4 100644 --- a/lexicons/com/atproto/label/subscribeLabels.json +++ b/lexicons/com/atproto/label/subscribeLabels.json @@ -17,21 +17,16 @@ "message": { "schema": { "type": "union", - "refs": [ - "#labels", - "#info" - ] + "refs": ["#labels", "#info"] } }, - "errors": [ - {"name": "FutureCursor"} - ] + "errors": [{ "name": "FutureCursor" }] }, "labels": { "type": "object", "required": ["seq", "labels"], "properties": { - "seq": {"type": "integer"}, + "seq": { "type": "integer" }, "labels": { "type": "array", "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } diff --git a/lexicons/com/atproto/moderation/createReport.json b/lexicons/com/atproto/moderation/createReport.json index c0c8d3b2594..0f34ed4329b 100644 --- a/lexicons/com/atproto/moderation/createReport.json +++ b/lexicons/com/atproto/moderation/createReport.json @@ -11,8 +11,11 @@ "type": "object", "required": ["reasonType", "subject"], "properties": { - "reasonType": {"type": "ref", "ref": "com.atproto.moderation.defs#reasonType"}, - "reason": {"type": "string"}, + "reasonType": { + "type": "ref", + "ref": "com.atproto.moderation.defs#reasonType" + }, + "reason": { "type": "string" }, "subject": { "type": "union", "refs": [ @@ -27,11 +30,20 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["id", "reasonType", "subject", "reportedBy", "createdAt"], + "required": [ + "id", + "reasonType", + "subject", + "reportedBy", + "createdAt" + ], "properties": { - "id": {"type": "integer"}, - "reasonType": {"type": "ref", "ref": "com.atproto.moderation.defs#reasonType"}, - "reason": {"type": "string"}, + "id": { "type": "integer" }, + "reasonType": { + "type": "ref", + "ref": "com.atproto.moderation.defs#reasonType" + }, + "reason": { "type": "string" }, "subject": { "type": "union", "refs": [ @@ -39,8 +51,8 @@ "com.atproto.repo.strongRef" ] }, - "reportedBy": {"type": "string", "format": "did"}, - "createdAt": {"type": "string", "format": "datetime"} + "reportedBy": { "type": "string", "format": "did" }, + "createdAt": { "type": "string", "format": "datetime" } } } } diff --git a/lexicons/com/atproto/moderation/defs.json b/lexicons/com/atproto/moderation/defs.json index 3d4e5455565..a06579a502e 100644 --- a/lexicons/com/atproto/moderation/defs.json +++ b/lexicons/com/atproto/moderation/defs.json @@ -1,41 +1,41 @@ { - "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" - ] - }, - "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" - } + "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" + ] + }, + "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" } + } } diff --git a/lexicons/com/atproto/repo/applyWrites.json b/lexicons/com/atproto/repo/applyWrites.json index 6d3a375b29d..5c37340cf3f 100644 --- a/lexicons/com/atproto/repo/applyWrites.json +++ b/lexicons/com/atproto/repo/applyWrites.json @@ -23,7 +23,11 @@ }, "writes": { "type": "array", - "items": {"type": "union", "refs": ["#create", "#update", "#delete"], "closed": true} + "items": { + "type": "union", + "refs": ["#create", "#update", "#delete"], + "closed": true + } }, "swapCommit": { "type": "string", @@ -32,18 +36,16 @@ } } }, - "errors": [ - {"name": "InvalidSwap"} - ] + "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"} + "collection": { "type": "string", "format": "nsid" }, + "rkey": { "type": "string", "maxLength": 15 }, + "value": { "type": "unknown" } } }, "update": { @@ -51,9 +53,9 @@ "description": "Update an existing record.", "required": ["collection", "rkey", "value"], "properties": { - "collection": {"type": "string", "format": "nsid"}, - "rkey": {"type": "string"}, - "value": {"type": "unknown"} + "collection": { "type": "string", "format": "nsid" }, + "rkey": { "type": "string" }, + "value": { "type": "unknown" } } }, "delete": { @@ -61,8 +63,8 @@ "description": "Delete an existing record.", "required": ["collection", "rkey"], "properties": { - "collection": {"type": "string", "format": "nsid"}, - "rkey": {"type": "string"} + "collection": { "type": "string", "format": "nsid" }, + "rkey": { "type": "string" } } } } diff --git a/lexicons/com/atproto/repo/createRecord.json b/lexicons/com/atproto/repo/createRecord.json index 3988de24b00..e594db58d51 100644 --- a/lexicons/com/atproto/repo/createRecord.json +++ b/lexicons/com/atproto/repo/createRecord.json @@ -49,14 +49,12 @@ "type": "object", "required": ["uri", "cid"], "properties": { - "uri": {"type": "string", "format": "at-uri"}, - "cid": {"type": "string", "format": "cid"} + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" } } } }, - "errors": [ - {"name": "InvalidSwap"} - ] + "errors": [{ "name": "InvalidSwap" }] } } } diff --git a/lexicons/com/atproto/repo/deleteRecord.json b/lexicons/com/atproto/repo/deleteRecord.json index 3735b02c5a0..53ef72b190e 100644 --- a/lexicons/com/atproto/repo/deleteRecord.json +++ b/lexicons/com/atproto/repo/deleteRecord.json @@ -38,9 +38,7 @@ } } }, - "errors": [ - {"name": "InvalidSwap"} - ] + "errors": [{ "name": "InvalidSwap" }] } } } diff --git a/lexicons/com/atproto/repo/describeRepo.json b/lexicons/com/atproto/repo/describeRepo.json index 51ec1138b25..b7f283bff70 100644 --- a/lexicons/com/atproto/repo/describeRepo.json +++ b/lexicons/com/atproto/repo/describeRepo.json @@ -20,16 +20,25 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["handle", "did", "didDoc", "collections", "handleIsCorrect"], + "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"} + "handle": { "type": "string", "format": "handle" }, + "did": { "type": "string", "format": "did" }, + "didDoc": { "type": "unknown" }, + "collections": { + "type": "array", + "items": { "type": "string", "format": "nsid" } + }, + "handleIsCorrect": { "type": "boolean" } } } } } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/repo/getRecord.json b/lexicons/com/atproto/repo/getRecord.json index 25032a522de..ec4d17e4260 100644 --- a/lexicons/com/atproto/repo/getRecord.json +++ b/lexicons/com/atproto/repo/getRecord.json @@ -19,7 +19,7 @@ "format": "nsid", "description": "The NSID of the record collection." }, - "rkey": {"type": "string", "description": "The key of the record."}, + "rkey": { "type": "string", "description": "The key of the record." }, "cid": { "type": "string", "format": "cid", @@ -33,9 +33,9 @@ "type": "object", "required": ["uri", "value"], "properties": { - "uri": {"type": "string", "format": "at-uri"}, - "cid": {"type": "string", "format": "cid"}, - "value": {"type": "unknown"} + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "value": { "type": "unknown" } } } } diff --git a/lexicons/com/atproto/repo/listRecords.json b/lexicons/com/atproto/repo/listRecords.json index 81c0d5a4270..8bcee4fcb58 100644 --- a/lexicons/com/atproto/repo/listRecords.json +++ b/lexicons/com/atproto/repo/listRecords.json @@ -9,13 +9,36 @@ "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": "Reverse the order of the returned records?"} + "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": "Reverse the order of the returned records?" + } } }, "output": { @@ -24,10 +47,10 @@ "type": "object", "required": ["records"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "records": { "type": "array", - "items": {"type": "ref", "ref": "#record"} + "items": { "type": "ref", "ref": "#record" } } } } @@ -37,10 +60,10 @@ "type": "object", "required": ["uri", "cid", "value"], "properties": { - "uri": {"type": "string", "format": "at-uri"}, - "cid": {"type": "string", "format": "cid"}, - "value": {"type": "unknown"} + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" }, + "value": { "type": "unknown" } } } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/repo/putRecord.json b/lexicons/com/atproto/repo/putRecord.json index d0067110fd5..118b41dc49c 100644 --- a/lexicons/com/atproto/repo/putRecord.json +++ b/lexicons/com/atproto/repo/putRecord.json @@ -55,14 +55,12 @@ "type": "object", "required": ["uri", "cid"], "properties": { - "uri": {"type": "string", "format": "at-uri"}, - "cid": {"type": "string", "format": "cid"} + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" } } } }, - "errors": [ - {"name": "InvalidSwap"} - ] + "errors": [{ "name": "InvalidSwap" }] } } } diff --git a/lexicons/com/atproto/repo/strongRef.json b/lexicons/com/atproto/repo/strongRef.json index 3bac786b4f4..cb79625165c 100644 --- a/lexicons/com/atproto/repo/strongRef.json +++ b/lexicons/com/atproto/repo/strongRef.json @@ -7,9 +7,9 @@ "type": "object", "required": ["uri", "cid"], "properties": { - "uri": {"type": "string", "format": "at-uri"}, - "cid": {"type": "string", "format": "cid"} + "uri": { "type": "string", "format": "at-uri" }, + "cid": { "type": "string", "format": "cid" } } } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/repo/uploadBlob.json b/lexicons/com/atproto/repo/uploadBlob.json index ab04c87e076..63d1671bd3e 100644 --- a/lexicons/com/atproto/repo/uploadBlob.json +++ b/lexicons/com/atproto/repo/uploadBlob.json @@ -14,7 +14,7 @@ "type": "object", "required": ["blob"], "properties": { - "blob": {"type": "blob"} + "blob": { "type": "blob" } } } } diff --git a/lexicons/com/atproto/server/createAccount.json b/lexicons/com/atproto/server/createAccount.json index 4aadb7b5b5f..9fd09740fda 100644 --- a/lexicons/com/atproto/server/createAccount.json +++ b/lexicons/com/atproto/server/createAccount.json @@ -11,12 +11,12 @@ "type": "object", "required": ["handle", "email", "password"], "properties": { - "email": {"type": "string"}, - "handle": {"type": "string", "format": "handle"}, - "did": {"type": "string", "format": "did"}, - "inviteCode": {"type": "string"}, - "password": {"type": "string"}, - "recoveryKey": {"type": "string"} + "email": { "type": "string" }, + "handle": { "type": "string", "format": "handle" }, + "did": { "type": "string", "format": "did" }, + "inviteCode": { "type": "string" }, + "password": { "type": "string" }, + "recoveryKey": { "type": "string" } } } }, @@ -34,14 +34,14 @@ } }, "errors": [ - {"name": "InvalidHandle"}, - {"name": "InvalidPassword"}, - {"name": "InvalidInviteCode"}, - {"name": "HandleNotAvailable"}, - {"name": "UnsupportedDomain"}, - {"name": "UnresolvableDid"}, - {"name": "IncompatibleDidDoc"} + { "name": "InvalidHandle" }, + { "name": "InvalidPassword" }, + { "name": "InvalidInviteCode" }, + { "name": "HandleNotAvailable" }, + { "name": "UnsupportedDomain" }, + { "name": "UnresolvableDid" }, + { "name": "IncompatibleDidDoc" } ] } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/server/createAppPassword.json b/lexicons/com/atproto/server/createAppPassword.json index ae7810e7e0e..4dfdcca1f6b 100644 --- a/lexicons/com/atproto/server/createAppPassword.json +++ b/lexicons/com/atproto/server/createAppPassword.json @@ -11,7 +11,7 @@ "type": "object", "required": ["name"], "properties": { - "name": {"type": "string"} + "name": { "type": "string" } } } }, @@ -22,17 +22,15 @@ "ref": "#appPassword" } }, - "errors": [ - {"name": "AccountTakedown"} - ] + "errors": [{ "name": "AccountTakedown" }] }, "appPassword": { "type": "object", "required": ["name", "password", "createdAt"], "properties": { - "name": {"type": "string"}, - "password": {"type": "string"}, - "createdAt": {"type": "string", "format": "datetime"} + "name": { "type": "string" }, + "password": { "type": "string" }, + "createdAt": { "type": "string", "format": "datetime" } } } } diff --git a/lexicons/com/atproto/server/createInviteCode.json b/lexicons/com/atproto/server/createInviteCode.json index 81a967d8a30..5ce5dbe39f9 100644 --- a/lexicons/com/atproto/server/createInviteCode.json +++ b/lexicons/com/atproto/server/createInviteCode.json @@ -11,8 +11,8 @@ "type": "object", "required": ["useCount"], "properties": { - "useCount": {"type": "integer"}, - "forAccount": {"type": "string", "format": "did"} + "useCount": { "type": "integer" }, + "forAccount": { "type": "string", "format": "did" } } } }, diff --git a/lexicons/com/atproto/server/createInviteCodes.json b/lexicons/com/atproto/server/createInviteCodes.json index b967ca2f0d3..827d798a088 100644 --- a/lexicons/com/atproto/server/createInviteCodes.json +++ b/lexicons/com/atproto/server/createInviteCodes.json @@ -11,11 +11,11 @@ "type": "object", "required": ["codeCount", "useCount"], "properties": { - "codeCount": {"type": "integer", "default": 1}, - "useCount": {"type": "integer"}, + "codeCount": { "type": "integer", "default": 1 }, + "useCount": { "type": "integer" }, "forAccounts": { "type": "array", - "items": {"type": "string", "format": "did"} + "items": { "type": "string", "format": "did" } } } } @@ -26,9 +26,9 @@ "type": "object", "required": ["codes"], "properties": { - "codes": { + "codes": { "type": "array", - "items": {"type": "ref", "ref": "#accountCodes"} + "items": { "type": "ref", "ref": "#accountCodes" } } } } @@ -38,10 +38,10 @@ "type": "object", "required": ["account", "codes"], "properties": { - "account": {"type": "string"}, - "codes": { + "account": { "type": "string" }, + "codes": { "type": "array", - "items": {"type": "string"} + "items": { "type": "string" } } } } diff --git a/lexicons/com/atproto/server/createSession.json b/lexicons/com/atproto/server/createSession.json index 0494d67a828..fc416ddabae 100644 --- a/lexicons/com/atproto/server/createSession.json +++ b/lexicons/com/atproto/server/createSession.json @@ -15,7 +15,7 @@ "type": "string", "description": "Handle or other identifier supported by the server for the authenticating user." }, - "password": {"type": "string"} + "password": { "type": "string" } } } }, @@ -25,17 +25,15 @@ "type": "object", "required": ["accessJwt", "refreshJwt", "handle", "did"], "properties": { - "accessJwt": {"type": "string"}, - "refreshJwt": {"type": "string"}, - "handle": {"type": "string", "format": "handle"}, - "did": {"type": "string", "format": "did"}, - "email": {"type": "string"} + "accessJwt": { "type": "string" }, + "refreshJwt": { "type": "string" }, + "handle": { "type": "string", "format": "handle" }, + "did": { "type": "string", "format": "did" }, + "email": { "type": "string" } } } }, - "errors": [ - {"name": "AccountTakedown"} - ] + "errors": [{ "name": "AccountTakedown" }] } } } diff --git a/lexicons/com/atproto/server/defs.json b/lexicons/com/atproto/server/defs.json index beb9954bb5b..8686d4d410c 100644 --- a/lexicons/com/atproto/server/defs.json +++ b/lexicons/com/atproto/server/defs.json @@ -4,17 +4,25 @@ "defs": { "inviteCode": { "type": "object", - "required": ["code", "available", "disabled", "forAccount", "createdBy", "createdAt", "uses"], + "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"}, + "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": "#inviteCodeUse"} + "items": { "type": "ref", "ref": "#inviteCodeUse" } } } }, @@ -22,8 +30,8 @@ "type": "object", "required": ["usedBy", "usedAt"], "properties": { - "usedBy": {"type": "string", "format": "did"}, - "usedAt": {"type": "string", "format": "datetime"} + "usedBy": { "type": "string", "format": "did" }, + "usedAt": { "type": "string", "format": "datetime" } } } } diff --git a/lexicons/com/atproto/server/deleteAccount.json b/lexicons/com/atproto/server/deleteAccount.json index f273ebb5fdf..2cef799aefe 100644 --- a/lexicons/com/atproto/server/deleteAccount.json +++ b/lexicons/com/atproto/server/deleteAccount.json @@ -9,7 +9,7 @@ "encoding": "application/json", "schema": { "type": "object", - "required": ["did", "password", "token"], + "required": ["did", "password", "token"], "properties": { "did": { "type": "string", "format": "did" }, "password": { "type": "string" }, @@ -20,4 +20,4 @@ "errors": [{ "name": "ExpiredToken" }, { "name": "InvalidToken" }] } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/server/deleteSession.json b/lexicons/com/atproto/server/deleteSession.json index de044bbdb6d..e05d019024a 100644 --- a/lexicons/com/atproto/server/deleteSession.json +++ b/lexicons/com/atproto/server/deleteSession.json @@ -7,4 +7,4 @@ "description": "Delete the current session." } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/server/describeServer.json b/lexicons/com/atproto/server/describeServer.json index dc291e5fc95..b19b1504020 100644 --- a/lexicons/com/atproto/server/describeServer.json +++ b/lexicons/com/atproto/server/describeServer.json @@ -11,9 +11,12 @@ "type": "object", "required": ["availableUserDomains"], "properties": { - "inviteCodeRequired": {"type": "boolean"}, - "availableUserDomains": {"type": "array", "items": {"type": "string"}}, - "links": {"type": "ref", "ref": "#links"} + "inviteCodeRequired": { "type": "boolean" }, + "availableUserDomains": { + "type": "array", + "items": { "type": "string" } + }, + "links": { "type": "ref", "ref": "#links" } } } } @@ -21,9 +24,9 @@ "links": { "type": "object", "properties": { - "privacyPolicy": {"type": "string"}, - "termsOfService": {"type": "string"} + "privacyPolicy": { "type": "string" }, + "termsOfService": { "type": "string" } } } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/server/getAccountInviteCodes.json b/lexicons/com/atproto/server/getAccountInviteCodes.json index ff6e4d6d1cb..a99f78e0d7b 100644 --- a/lexicons/com/atproto/server/getAccountInviteCodes.json +++ b/lexicons/com/atproto/server/getAccountInviteCodes.json @@ -28,9 +28,7 @@ } } }, - "errors": [ - {"name": "DuplicateCreate"} - ] + "errors": [{ "name": "DuplicateCreate" }] } } } diff --git a/lexicons/com/atproto/server/getSession.json b/lexicons/com/atproto/server/getSession.json index db790f39e05..55b129be3df 100644 --- a/lexicons/com/atproto/server/getSession.json +++ b/lexicons/com/atproto/server/getSession.json @@ -11,12 +11,12 @@ "type": "object", "required": ["handle", "did"], "properties": { - "handle": {"type": "string", "format": "handle"}, - "did": {"type": "string", "format": "did"}, - "email": {"type": "string"} + "handle": { "type": "string", "format": "handle" }, + "did": { "type": "string", "format": "did" }, + "email": { "type": "string" } } } } } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/server/listAppPasswords.json b/lexicons/com/atproto/server/listAppPasswords.json index 613ca17ae43..f50a13d6b82 100644 --- a/lexicons/com/atproto/server/listAppPasswords.json +++ b/lexicons/com/atproto/server/listAppPasswords.json @@ -13,21 +13,19 @@ "properties": { "passwords": { "type": "array", - "items": {"type": "ref", "ref": "#appPassword"} + "items": { "type": "ref", "ref": "#appPassword" } } } } }, - "errors": [ - {"name": "AccountTakedown"} - ] + "errors": [{ "name": "AccountTakedown" }] }, "appPassword": { "type": "object", "required": ["name", "createdAt"], "properties": { - "name": {"type": "string"}, - "createdAt": {"type": "string", "format": "datetime"} + "name": { "type": "string" }, + "createdAt": { "type": "string", "format": "datetime" } } } } diff --git a/lexicons/com/atproto/server/refreshSession.json b/lexicons/com/atproto/server/refreshSession.json index 18ab3ed4777..ab895a34c94 100644 --- a/lexicons/com/atproto/server/refreshSession.json +++ b/lexicons/com/atproto/server/refreshSession.json @@ -11,16 +11,14 @@ "type": "object", "required": ["accessJwt", "refreshJwt", "handle", "did"], "properties": { - "accessJwt": {"type": "string"}, - "refreshJwt": {"type": "string"}, - "handle": {"type": "string", "format": "handle"}, - "did": {"type": "string", "format": "did"} + "accessJwt": { "type": "string" }, + "refreshJwt": { "type": "string" }, + "handle": { "type": "string", "format": "handle" }, + "did": { "type": "string", "format": "did" } } } }, - "errors": [ - {"name": "AccountTakedown"} - ] + "errors": [{ "name": "AccountTakedown" }] } } } diff --git a/lexicons/com/atproto/server/requestAccountDelete.json b/lexicons/com/atproto/server/requestAccountDelete.json index fcb9a869d5d..aaac0b70ec9 100644 --- a/lexicons/com/atproto/server/requestAccountDelete.json +++ b/lexicons/com/atproto/server/requestAccountDelete.json @@ -7,4 +7,4 @@ "description": "Initiate a user account deletion via email." } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/server/revokeAppPassword.json b/lexicons/com/atproto/server/revokeAppPassword.json index 265f89544e7..52094c4459c 100644 --- a/lexicons/com/atproto/server/revokeAppPassword.json +++ b/lexicons/com/atproto/server/revokeAppPassword.json @@ -11,7 +11,7 @@ "type": "object", "required": ["name"], "properties": { - "name": {"type": "string"} + "name": { "type": "string" } } } } diff --git a/lexicons/com/atproto/sync/getBlob.json b/lexicons/com/atproto/sync/getBlob.json index 9b29b16a6e8..23e18a4f3b5 100644 --- a/lexicons/com/atproto/sync/getBlob.json +++ b/lexicons/com/atproto/sync/getBlob.json @@ -9,8 +9,16 @@ "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"} + "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": { diff --git a/lexicons/com/atproto/sync/getBlocks.json b/lexicons/com/atproto/sync/getBlocks.json index cddacb56ad5..0b6c25f8252 100644 --- a/lexicons/com/atproto/sync/getBlocks.json +++ b/lexicons/com/atproto/sync/getBlocks.json @@ -15,8 +15,8 @@ "description": "The DID of the repo." }, "cids": { - "type": "array", - "items": {"type": "string", "format": "cid"} + "type": "array", + "items": { "type": "string", "format": "cid" } } } }, @@ -25,4 +25,4 @@ } } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/sync/getCheckout.json b/lexicons/com/atproto/sync/getCheckout.json index 0f57cb377cd..b3fb375e5db 100644 --- a/lexicons/com/atproto/sync/getCheckout.json +++ b/lexicons/com/atproto/sync/getCheckout.json @@ -21,4 +21,4 @@ } } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/sync/getHead.json b/lexicons/com/atproto/sync/getHead.json index bfd110e019b..31ed72cbae1 100644 --- a/lexicons/com/atproto/sync/getHead.json +++ b/lexicons/com/atproto/sync/getHead.json @@ -22,13 +22,11 @@ "type": "object", "required": ["root"], "properties": { - "root": {"type": "string", "format": "cid"} + "root": { "type": "string", "format": "cid" } } } }, - "errors": [ - {"name": "HeadNotFound"} - ] + "errors": [{ "name": "HeadNotFound" }] } } } diff --git a/lexicons/com/atproto/sync/getLatestCommit.json b/lexicons/com/atproto/sync/getLatestCommit.json index 8beebdaa434..602cc2dac59 100644 --- a/lexicons/com/atproto/sync/getLatestCommit.json +++ b/lexicons/com/atproto/sync/getLatestCommit.json @@ -22,14 +22,12 @@ "type": "object", "required": ["cid", "rev"], "properties": { - "cid": {"type": "string", "format": "cid"}, - "rev": {"type": "string"} + "cid": { "type": "string", "format": "cid" }, + "rev": { "type": "string" } } } }, - "errors": [ - {"name": "RepoNotFound"} - ] + "errors": [{ "name": "RepoNotFound" }] } } } diff --git a/lexicons/com/atproto/sync/getRecord.json b/lexicons/com/atproto/sync/getRecord.json index 6ff65cfbc25..ea14ba0f75e 100644 --- a/lexicons/com/atproto/sync/getRecord.json +++ b/lexicons/com/atproto/sync/getRecord.json @@ -14,8 +14,8 @@ "format": "did", "description": "The DID of the repo." }, - "collection": {"type": "string", "format": "nsid"}, - "rkey": {"type": "string" }, + "collection": { "type": "string", "format": "nsid" }, + "rkey": { "type": "string" }, "commit": { "type": "string", "format": "cid", @@ -28,4 +28,4 @@ } } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/sync/listBlobs.json b/lexicons/com/atproto/sync/listBlobs.json index 7ca3039f6d0..01e01391256 100644 --- a/lexicons/com/atproto/sync/listBlobs.json +++ b/lexicons/com/atproto/sync/listBlobs.json @@ -9,10 +9,23 @@ "type": "params", "required": ["did"], "properties": { - "did": {"type": "string", "format": "did", "description": "The DID of the repo."}, - "since": { "type": "string", "format": "cid", "description": "Optional revision of the repo to list blobs since"}, - "limit": {"type": "integer", "minimum": 1, "maximum": 1000, "default": 500}, - "cursor": {"type": "string"} + "did": { + "type": "string", + "format": "did", + "description": "The DID of the repo." + }, + "since": { + "type": "string", + "format": "cid", + "description": "Optional revision of the repo to list blobs since" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "default": 500 + }, + "cursor": { "type": "string" } } }, "output": { @@ -21,7 +34,7 @@ "type": "object", "required": ["cids"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "cids": { "type": "array", "items": { "type": "string", "format": "cid" } @@ -31,4 +44,4 @@ } } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/sync/listRepos.json b/lexicons/com/atproto/sync/listRepos.json index e54f74e50a1..9fdd57a55cd 100644 --- a/lexicons/com/atproto/sync/listRepos.json +++ b/lexicons/com/atproto/sync/listRepos.json @@ -8,8 +8,13 @@ "parameters": { "type": "params", "properties": { - "limit": {"type": "integer", "minimum": 1, "maximum": 1000, "default": 500}, - "cursor": {"type": "string"} + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "default": 500 + }, + "cursor": { "type": "string" } } }, "output": { @@ -18,10 +23,10 @@ "type": "object", "required": ["repos"], "properties": { - "cursor": {"type": "string"}, + "cursor": { "type": "string" }, "repos": { "type": "array", - "items": {"type": "ref", "ref": "#repo"} + "items": { "type": "ref", "ref": "#repo" } } } } @@ -36,4 +41,4 @@ } } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/sync/notifyOfUpdate.json b/lexicons/com/atproto/sync/notifyOfUpdate.json index a5449b152fe..caca6fe9f30 100644 --- a/lexicons/com/atproto/sync/notifyOfUpdate.json +++ b/lexicons/com/atproto/sync/notifyOfUpdate.json @@ -11,10 +11,13 @@ "type": "object", "required": ["hostname"], "properties": { - "hostname": {"type": "string", "description": "Hostname of the service that is notifying of update."} + "hostname": { + "type": "string", + "description": "Hostname of the service that is notifying of update." + } } } } } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/sync/requestCrawl.json b/lexicons/com/atproto/sync/requestCrawl.json index 1bd09829002..a3520a33180 100644 --- a/lexicons/com/atproto/sync/requestCrawl.json +++ b/lexicons/com/atproto/sync/requestCrawl.json @@ -11,10 +11,13 @@ "type": "object", "required": ["hostname"], "properties": { - "hostname": {"type": "string", "description": "Hostname of the service that is requesting to be crawled."} + "hostname": { + "type": "string", + "description": "Hostname of the service that is requesting to be crawled." + } } } } } } -} \ No newline at end of file +} diff --git a/lexicons/com/atproto/sync/subscribeRepos.json b/lexicons/com/atproto/sync/subscribeRepos.json index ee3e88a4833..b8feecad5b8 100644 --- a/lexicons/com/atproto/sync/subscribeRepos.json +++ b/lexicons/com/atproto/sync/subscribeRepos.json @@ -17,31 +17,34 @@ "message": { "schema": { "type": "union", - "refs": [ - "#commit", - "#handle", - "#migrate", - "#tombstone", - "#info" - ] + "refs": ["#commit", "#handle", "#migrate", "#tombstone", "#info"] } }, - "errors": [ - {"name": "FutureCursor"}, - {"name": "ConsumerTooSlow"} - ] + "errors": [{ "name": "FutureCursor" }, { "name": "ConsumerTooSlow" }] }, "commit": { "type": "object", - "required": ["seq", "rebase", "tooBig", "repo", "commit", "rev", "since", "blocks", "ops", "blobs", "time"], + "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"}, + "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" @@ -56,25 +59,25 @@ "maxLength": 1000000 }, "ops": { - "type": "array", - "items": { "type": "ref", "ref": "#repoOp"}, + "type": "array", + "items": { "type": "ref", "ref": "#repoOp" }, "maxLength": 200 }, "blobs": { "type": "array", - "items": {"type": "cid-link"} + "items": { "type": "cid-link" } }, - "time": {"type": "string", "format": "datetime"} + "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"} + "seq": { "type": "integer" }, + "did": { "type": "string", "format": "did" }, + "handle": { "type": "string", "format": "handle" }, + "time": { "type": "string", "format": "datetime" } } }, "migrate": { @@ -82,19 +85,19 @@ "required": ["seq", "did", "migrateTo", "time"], "nullable": ["migrateTo"], "properties": { - "seq": {"type": "integer"}, - "did": {"type": "string", "format": "did"}, - "migrateTo": {"type": "string"}, - "time": {"type": "string", "format": "datetime"} + "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"} + "seq": { "type": "integer" }, + "did": { "type": "string", "format": "did" }, + "time": { "type": "string", "format": "datetime" } } }, "info": { @@ -118,14 +121,10 @@ "properties": { "action": { "type": "string", - "knownValues": [ - "create", - "update", - "delete" - ] + "knownValues": ["create", "update", "delete"] }, - "path": {"type": "string"}, - "cid": {"type": "cid-link"} + "path": { "type": "string" }, + "cid": { "type": "cid-link" } } } } diff --git a/package.json b/package.json index 7a6b3ac6fb9..13d9555429f 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,10 @@ "node": ">=18" }, "scripts": { - "verify": "pnpm -r --stream lint && pnpm -r --stream prettier", - "format": "pnpm -r --stream prettier:fix", + "lint:fix": "pnpm lint --fix", + "lint": "eslint . --ext .ts,.tsx", + "verify": "prettier --check . && pnpm lint", + "format": "prettier --write .", "build": "pnpm -r --stream build", "update-main-to-dist": "pnpm -r --stream update-main-to-dist", "test": "LOG_ENABLED=false NODE_ENV=development ./packages/dev-infra/with-test-redis-and-db.sh pnpm --stream -r test", @@ -41,7 +43,6 @@ "eslint-plugin-prettier": "^4.2.1", "jest": "^28.1.2", "node-gyp": "^9.3.1", - "npm-run-all": "^4.1.5", "pino-pretty": "^9.1.0", "prettier": "^2.7.1", "prettier-config-standard": "^5.0.0", diff --git a/packages/README.md b/packages/README.md index ba77e031e8c..68835de45f7 100644 --- a/packages/README.md +++ b/packages/README.md @@ -26,7 +26,7 @@ You can run benchmarks with `pnpm bench`. ### Attaching a profiler Running `pnpm bench:profile` will launch `bench` with `--inspect-brk` flag. -Execution will be paused until a debugger is attached, you can read more +Execution will be paused until a debugger is attached, you can read more about node debuggers [here](https://nodejs.org/en/docs/guides/debugging-getting-started#inspector-clients) An easy way to profile is: diff --git a/packages/api/README.md b/packages/api/README.md index 843da40459f..fdfcbc48b73 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -34,16 +34,16 @@ import { BskyAgent, AtpSessionEvent, AtpSessionData } from '@atproto/api' const agent = new BskyAgent({ service: 'https://example.com', persistSession: (evt: AtpSessionEvent, sess?: AtpSessionData) => { - // store the session-data for reuse - } + // store the session-data for reuse + }, }) -await agent.login({identifier: 'alice@mail.com', password: 'hunter2'}) +await agent.login({ identifier: 'alice@mail.com', password: 'hunter2' }) await agent.resumeSession(savedSessionData) await agent.createAccount({ email: 'alice@mail.com', password: 'hunter2', - handle: 'alice.example.com' + handle: 'alice.example.com', }) ``` @@ -127,16 +127,18 @@ Some records (ie posts) use the `app.bsky.richtext` lexicon. At the moment richt ℹ️ It is **strongly** recommended to use this package's `RichText` library. Javascript encodes strings in utf16 while the protocol (and most other programming environments) use utf8. Converting between the two is challenging, but `RichText` handles that for you. ```typescript -import {RichText} from '@atproto/api' +import { RichText } from '@atproto/api' // creating richtext -const rt = new RichText({text: 'Hello @alice.com, check out this link: https://example.com'}) +const rt = new RichText({ + text: 'Hello @alice.com, check out this link: https://example.com', +}) await rt.detectFacets(agent) // automatically detects mentions and links const postRecord = { $type: 'app.bsky.feed.post', text: rt.text, facets: rt.facets, - createdAt: new Date().toISOString() + createdAt: new Date().toISOString(), } // rendering as markdown @@ -152,10 +154,10 @@ for (const segment of rt.segments()) { } // calculating string lengths -const rt2 = new RichText({text: 'Hello'}) +const rt2 = new RichText({ text: 'Hello' }) console.log(rt2.length) // => 5 console.log(rt2.graphemeLength) // => 5 -const rt3 = new RichText({text: '👨‍👩‍👧‍👧'}) +const rt3 = new RichText({ text: '👨‍👩‍👧‍👧' }) console.log(rt3.length) // => 25 console.log(rt3.graphemeLength) // => 1 ``` @@ -171,12 +173,12 @@ Applying the moderation system is a challenging task, but we've done our best to For more information, see the [Moderation Documentation](./docs/moderation.md) or the associated [Labels Reference](./docs/labels.md). ```typescript -import {moderatePost, moderateProfile} from '@atproto/api' +import { moderatePost, moderateProfile } from '@atproto/api' // We call the appropriate moderation function for the content // = -const postMod = moderatePost(postView, getOpts()) +const postMod = moderatePost(postView, getOpts()) const profileMod = moderateProfile(profileView, getOpts()) // We then use the output to decide how to affect rendering @@ -235,16 +237,16 @@ function getOpts() { { labeler: { did: '...', - displayName: 'My mod service' + displayName: 'My mod service', }, labels: { porn: 'hide', sexual: 'warn', nudity: 'ignore', // ... - } - } - ] + }, + }, + ], } } ``` @@ -258,24 +260,28 @@ The methods above are convenience wrappers. It covers most but not all available The AT Protocol identifies methods and records with reverse-DNS names. You can use them on the agent as well: ```typescript -const res1 = await agent.com.atproto.repo.createRecord( +const res1 = await agent.com.atproto.repo.createRecord({ + did: alice.did, + collection: 'app.bsky.feed.post', + record: { + $type: 'app.bsky.feed.post', + text: 'Hello, world!', + createdAt: new Date().toISOString(), + }, +}) +const res2 = await agent.com.atproto.repo.listRecords({ + repo: alice.did, + collection: 'app.bsky.feed.post', +}) + +const res3 = await agent.app.bsky.feed.post.create( + { repo: alice.did }, { - did: alice.did, - collection: 'app.bsky.feed.post', - record: { - $type: 'app.bsky.feed.post', - text: 'Hello, world!', - createdAt: new Date().toISOString() - } - } + text: 'Hello, world!', + createdAt: new Date().toISOString(), + }, ) -const res2 = await agent.com.atproto.repo.listRecords({repo: alice.did, collection: 'app.bsky.feed.post'}) - -const res3 = await agent.app.bsky.feed.post.create({repo: alice.did}, { - text: 'Hello, world!', - createdAt: new Date().toISOString() -}) -const res4 = await agent.app.bsky.feed.post.list({repo: alice.did}) +const res4 = await agent.app.bsky.feed.post.list({ repo: alice.did }) ``` ### Generic agent @@ -285,7 +291,7 @@ If you want a generic AT Protocol agent without methods related to the Bluesky s ```typescript import { AtpAgent } from '@atproto/api' -const agent = new AtpAgent({service: 'https://example.com'}) +const agent = new AtpAgent({ service: 'https://example.com' }) ``` ### Non-browser configuration @@ -295,10 +301,13 @@ In non-browser environments you'll need to specify a fetch polyfill. [See the ex ```typescript import { BskyAgent } from '@atproto/api' -const agent = new BskyAgent({service: 'https://example.com'}) +const agent = new BskyAgent({ service: 'https://example.com' }) // provide a custom fetch implementation (shouldnt be needed in node or the browser) -import {AtpAgentFetchHeaders, AtpAgentFetchHandlerResponse} from '@atproto/api' +import { + AtpAgentFetchHeaders, + AtpAgentFetchHandlerResponse, +} from '@atproto/api' BskyAgent.configure({ async fetch( httpUri: string, @@ -307,8 +316,8 @@ BskyAgent.configure({ httpReqBody: any, ): Promise { // insert definition here... - return {status: 200, /*...*/} - } + return { status: 200 /*...*/ } + }, }) ``` diff --git a/packages/api/bench/agent.bench.ts b/packages/api/bench/agent.bench.ts index 22905e19f06..333fbd49a20 100644 --- a/packages/api/bench/agent.bench.ts +++ b/packages/api/bench/agent.bench.ts @@ -1,9 +1,9 @@ -import { BskyAgent } from "@atproto/api"; +import { BskyAgent } from '@atproto/api' describe('Agent Benchmarks', () => { it('Creates new Agent instance 10 times', () => { for (let i = 0; i < 10; i++) { - new BskyAgent({ service: 'https://bsky.social' }); + new BskyAgent({ service: 'https://bsky.social' }) } }) }) diff --git a/packages/api/definitions/labels.json b/packages/api/definitions/labels.json index e5ab7d2cd36..91b5dd43cba 100644 --- a/packages/api/definitions/labels.json +++ b/packages/api/definitions/labels.json @@ -215,4 +215,4 @@ } ] } -] \ No newline at end of file +] diff --git a/packages/api/definitions/locale/en/label-groups.json b/packages/api/definitions/locale/en/label-groups.json index 5658f0cedea..06cc6699a7b 100644 --- a/packages/api/definitions/locale/en/label-groups.json +++ b/packages/api/definitions/locale/en/label-groups.json @@ -35,4 +35,4 @@ "name": "Misinformation", "description": "Content which misleads or defrauds users." } -} \ No newline at end of file +} diff --git a/packages/api/definitions/locale/en/proposed-label-groups.json b/packages/api/definitions/locale/en/proposed-label-groups.json index 5658f0cedea..06cc6699a7b 100644 --- a/packages/api/definitions/locale/en/proposed-label-groups.json +++ b/packages/api/definitions/locale/en/proposed-label-groups.json @@ -35,4 +35,4 @@ "name": "Misinformation", "description": "Content which misleads or defrauds users." } -} \ No newline at end of file +} diff --git a/packages/api/definitions/locale/en/proposed-labels.json b/packages/api/definitions/locale/en/proposed-labels.json index 48702af87a2..e789103dfc4 100644 --- a/packages/api/definitions/locale/en/proposed-labels.json +++ b/packages/api/definitions/locale/en/proposed-labels.json @@ -629,4 +629,4 @@ "description": "The moderators believe this content includes statements which are not technically wrong or lying, but are misleading through omission or re-contextualization." } } -} \ No newline at end of file +} diff --git a/packages/api/definitions/post-moderation-behaviors.json b/packages/api/definitions/post-moderation-behaviors.json index 26df7863fbf..a82ae5c80d9 100644 --- a/packages/api/definitions/post-moderation-behaviors.json +++ b/packages/api/definitions/post-moderation-behaviors.json @@ -1,13 +1,47 @@ - { "users": { - "self": {"blocking": false, "blockedBy": false, "muted": false, "mutedByList": false}, - "alice": {"blocking": false, "blockedBy": false, "muted": false, "mutedByList": false}, - "bob": {"blocking": true, "blockedBy": false, "muted": false, "mutedByList": false}, - "carla": {"blocking": false, "blockedBy": true, "muted": false, "mutedByList": false}, - "dan": {"blocking": false, "blockedBy": false, "muted": true, "mutedByList": false}, - "elise": {"blocking": false, "blockedBy": false, "muted": false, "mutedByList": true}, - "fern": {"blocking": true, "blockedBy": true, "muted": false, "mutedByList": false} + "self": { + "blocking": false, + "blockedBy": false, + "muted": false, + "mutedByList": false + }, + "alice": { + "blocking": false, + "blockedBy": false, + "muted": false, + "mutedByList": false + }, + "bob": { + "blocking": true, + "blockedBy": false, + "muted": false, + "mutedByList": false + }, + "carla": { + "blocking": false, + "blockedBy": true, + "muted": false, + "mutedByList": false + }, + "dan": { + "blocking": false, + "blockedBy": false, + "muted": true, + "mutedByList": false + }, + "elise": { + "blocking": false, + "blockedBy": false, + "muted": false, + "mutedByList": true + }, + "fern": { + "blocking": true, + "blockedBy": true, + "muted": false, + "mutedByList": false + } }, "configurations": { "none": {}, @@ -15,46 +49,46 @@ "adultContentEnabled": false }, "intolerant-hide": { - "settings": {"intolerant": "hide"} + "settings": { "intolerant": "hide" } }, "intolerant-warn": { - "settings": {"intolerant": "warn"} + "settings": { "intolerant": "warn" } }, "intolerant-ignore": { - "settings": {"intolerant": "ignore"} + "settings": { "intolerant": "ignore" } }, "porn-hide": { "adultContentEnabled": true, - "settings": {"porn": "hide"} + "settings": { "porn": "hide" } }, "porn-warn": { "adultContentEnabled": true, - "settings": {"porn": "warn"} + "settings": { "porn": "warn" } }, "porn-ignore": { "adultContentEnabled": true, - "settings": {"porn": "ignore"} + "settings": { "porn": "ignore" } }, "scam-hide": { - "settings": {"scam": "hide"} + "settings": { "scam": "hide" } }, "scam-warn": { - "settings": {"scam": "warn"} + "settings": { "scam": "warn" } }, "scam-ignore": { - "settings": {"scam": "ignore"} + "settings": { "scam": "ignore" } }, "porn-hide-intolerant-hide": { "adultContentEnabled": true, - "settings": {"porn": "hide", "intolerant": "hide"} + "settings": { "porn": "hide", "intolerant": "hide" } }, "porn-hide-intolerant-warn": { "adultContentEnabled": true, - "settings": {"porn": "hide", "intolerant": "warn"} + "settings": { "porn": "hide", "intolerant": "warn" } }, "porn-warn-intolerant-hide": { "adultContentEnabled": true, - "settings": {"porn": "warn", "intolerant": "hide"} + "settings": { "porn": "warn", "intolerant": "hide" } } }, "scenarios": { @@ -62,28 +96,38 @@ "cfg": "none", "subject": "post", "author": "alice", - "labels": {"post": ["!hide"]}, + "labels": { "post": ["!hide"] }, "behaviors": { - "content": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true} + "content": { + "cause": "label:!hide", + "filter": true, + "blur": true, + "noOverride": true + } } }, "Imperative label ('!hide') on author profile": { "cfg": "none", "subject": "post", "author": "alice", - "labels": {"profile": ["!hide"]}, + "labels": { "profile": ["!hide"] }, "behaviors": { - "avatar": {"cause": "label:!hide", "blur": true, "noOverride": true} + "avatar": { "cause": "label:!hide", "blur": true, "noOverride": true } } }, "Imperative label ('!hide') on author account": { "cfg": "none", "subject": "post", "author": "alice", - "labels": {"account": ["!hide"]}, + "labels": { "account": ["!hide"] }, "behaviors": { - "content": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true}, - "avatar": {"cause": "label:!hide", "blur": true, "noOverride": true} + "content": { + "cause": "label:!hide", + "filter": true, + "blur": true, + "noOverride": true + }, + "avatar": { "cause": "label:!hide", "blur": true, "noOverride": true } } }, "Imperative label ('!hide') on quoted post": { @@ -91,10 +135,10 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedPost": ["!hide"]}, + "labels": { "quotedPost": ["!hide"] }, "behaviors": { - "content": {"cause": "label:!hide", "filter": true}, - "embed": {"cause": "label:!hide", "blur": true, "noOverride": true} + "content": { "cause": "label:!hide", "filter": true }, + "embed": { "cause": "label:!hide", "blur": true, "noOverride": true } } }, "Imperative label ('!hide') on quoted author account": { @@ -102,10 +146,10 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedAccount": ["!hide"]}, + "labels": { "quotedAccount": ["!hide"] }, "behaviors": { - "content": {"cause": "label:!hide", "filter": true}, - "embed": {"cause": "label:!hide", "blur": true, "noOverride": true} + "content": { "cause": "label:!hide", "filter": true }, + "embed": { "cause": "label:!hide", "blur": true, "noOverride": true } } }, @@ -113,25 +157,25 @@ "cfg": "none", "subject": "post", "author": "alice", - "labels": {"post": ["!no-promote"]}, + "labels": { "post": ["!no-promote"] }, "behaviors": { - "content": {"cause": "label:!no-promote", "filter": true} + "content": { "cause": "label:!no-promote", "filter": true } } }, "Imperative label ('!no-promote') on author profile": { "cfg": "none", "subject": "post", "author": "alice", - "labels": {"profile": ["!no-promote"]}, + "labels": { "profile": ["!no-promote"] }, "behaviors": {} }, "Imperative label ('!no-promote') on author account": { "cfg": "none", "subject": "post", "author": "alice", - "labels": {"account": ["!no-promote"]}, + "labels": { "account": ["!no-promote"] }, "behaviors": { - "content": {"cause": "label:!no-promote", "filter": true} + "content": { "cause": "label:!no-promote", "filter": true } } }, "Imperative label ('!no-promote') on quoted post": { @@ -139,9 +183,9 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedPost": ["!no-promote"]}, + "labels": { "quotedPost": ["!no-promote"] }, "behaviors": { - "content": {"cause": "label:!no-promote", "filter": true} + "content": { "cause": "label:!no-promote", "filter": true } } }, "Imperative label ('!no-promote') on quoted author account": { @@ -149,9 +193,9 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedAccount": ["!no-promote"]}, + "labels": { "quotedAccount": ["!no-promote"] }, "behaviors": { - "content": {"cause": "label:!no-promote", "filter": true} + "content": { "cause": "label:!no-promote", "filter": true } } }, @@ -159,28 +203,28 @@ "cfg": "none", "subject": "post", "author": "alice", - "labels": {"post": ["!warn"]}, + "labels": { "post": ["!warn"] }, "behaviors": { - "content": {"cause": "label:!warn", "blur": true} + "content": { "cause": "label:!warn", "blur": true } } }, "Imperative label ('!warn') on author profile": { "cfg": "none", "subject": "post", "author": "alice", - "labels": {"profile": ["!warn"]}, + "labels": { "profile": ["!warn"] }, "behaviors": { - "avatar": {"blur": true} + "avatar": { "blur": true } } }, "Imperative label ('!warn') on author account": { "cfg": "none", "subject": "post", "author": "alice", - "labels": {"account": ["!warn"]}, + "labels": { "account": ["!warn"] }, "behaviors": { - "content": {"cause": "label:!warn", "blur": true}, - "avatar": {"blur": true} + "content": { "cause": "label:!warn", "blur": true }, + "avatar": { "blur": true } } }, "Imperative label ('!warn') on quoted post": { @@ -188,9 +232,9 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedPost": ["!warn"]}, + "labels": { "quotedPost": ["!warn"] }, "behaviors": { - "embed": {"cause": "label:!warn", "blur": true} + "embed": { "cause": "label:!warn", "blur": true } } }, "Imperative label ('!warn') on quoted author account": { @@ -198,9 +242,9 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedAccount": ["!warn"]}, + "labels": { "quotedAccount": ["!warn"] }, "behaviors": { - "embed": {"cause": "label:!warn", "blur": true} + "embed": { "cause": "label:!warn", "blur": true } } }, @@ -208,28 +252,32 @@ "cfg": "intolerant-hide", "subject": "post", "author": "alice", - "labels": {"post": ["intolerant"]}, + "labels": { "post": ["intolerant"] }, "behaviors": { - "content": {"cause": "label:intolerant", "filter": true, "blur": true} + "content": { "cause": "label:intolerant", "filter": true, "blur": true } } }, "Blur label ('intolerant') on author profile (hide)": { "cfg": "intolerant-hide", "subject": "post", "author": "alice", - "labels": {"profile": ["intolerant"]}, + "labels": { "profile": ["intolerant"] }, "behaviors": { - "avatar": {"blur": true} + "avatar": { "blur": true } } }, "Blur label ('intolerant') on author account (hide)": { "cfg": "intolerant-hide", "subject": "post", "author": "alice", - "labels": {"account": ["intolerant"]}, + "labels": { "account": ["intolerant"] }, "behaviors": { - "content": {"cause": "label:intolerant", "filter": true, "blur": true}, - "avatar": {"blur": true} + "content": { + "cause": "label:intolerant", + "filter": true, + "blur": true + }, + "avatar": { "blur": true } } }, "Blur label ('intolerant') on quoted post (hide)": { @@ -237,10 +285,10 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedPost": ["intolerant"]}, + "labels": { "quotedPost": ["intolerant"] }, "behaviors": { - "content": {"cause": "label:intolerant", "filter": true}, - "embed": {"cause": "label:intolerant", "blur": true} + "content": { "cause": "label:intolerant", "filter": true }, + "embed": { "cause": "label:intolerant", "blur": true } } }, "Blur label ('intolerant') on quoted author account (hide)": { @@ -248,10 +296,10 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedAccount": ["intolerant"]}, + "labels": { "quotedAccount": ["intolerant"] }, "behaviors": { - "content": {"cause": "label:intolerant", "filter": true}, - "embed": {"cause": "label:intolerant", "blur": true} + "content": { "cause": "label:intolerant", "filter": true }, + "embed": { "cause": "label:intolerant", "blur": true } } }, @@ -259,28 +307,28 @@ "cfg": "intolerant-warn", "subject": "post", "author": "alice", - "labels": {"post": ["intolerant"]}, + "labels": { "post": ["intolerant"] }, "behaviors": { - "content": {"cause": "label:intolerant", "blur": true} + "content": { "cause": "label:intolerant", "blur": true } } }, "Blur label ('intolerant') on author profile (warn)": { "cfg": "intolerant-warn", "subject": "post", "author": "alice", - "labels": {"profile": ["intolerant"]}, + "labels": { "profile": ["intolerant"] }, "behaviors": { - "avatar": {"blur": true} + "avatar": { "blur": true } } }, "Blur label ('intolerant') on author account (warn)": { "cfg": "intolerant-warn", "subject": "post", "author": "alice", - "labels": {"account": ["intolerant"]}, + "labels": { "account": ["intolerant"] }, "behaviors": { - "content": {"cause": "label:intolerant", "blur": true}, - "avatar": {"blur": true} + "content": { "cause": "label:intolerant", "blur": true }, + "avatar": { "blur": true } } }, "Blur label ('intolerant') on quoted post (warn)": { @@ -288,9 +336,9 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedPost": ["intolerant"]}, + "labels": { "quotedPost": ["intolerant"] }, "behaviors": { - "embed": {"cause": "label:intolerant", "blur": true} + "embed": { "cause": "label:intolerant", "blur": true } } }, "Blur label ('intolerant') on quoted author account (warn)": { @@ -298,9 +346,9 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedPost": ["intolerant"]}, + "labels": { "quotedPost": ["intolerant"] }, "behaviors": { - "embed": {"cause": "label:intolerant", "blur": true} + "embed": { "cause": "label:intolerant", "blur": true } } }, @@ -308,21 +356,21 @@ "cfg": "intolerant-ignore", "subject": "post", "author": "alice", - "labels": {"post": ["intolerant"]}, + "labels": { "post": ["intolerant"] }, "behaviors": {} }, "Blur label ('intolerant') on author profile (ignore)": { "cfg": "intolerant-ignore", "subject": "post", "author": "alice", - "labels": {"profile": ["intolerant"]}, + "labels": { "profile": ["intolerant"] }, "behaviors": {} }, "Blur label ('intolerant') on author account (ignore)": { "cfg": "intolerant-ignore", "subject": "post", "author": "alice", - "labels": {"account": ["intolerant"]}, + "labels": { "account": ["intolerant"] }, "behaviors": {} }, "Blur label ('intolerant') on quoted post (ignore)": { @@ -330,7 +378,7 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedPost": ["intolerant"]}, + "labels": { "quotedPost": ["intolerant"] }, "behaviors": {} }, "Blur label ('intolerant') on quoted author account (ignore)": { @@ -338,7 +386,7 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedAccount": ["intolerant"]}, + "labels": { "quotedAccount": ["intolerant"] }, "behaviors": {} }, @@ -346,30 +394,30 @@ "cfg": "porn-hide", "subject": "post", "author": "alice", - "labels": {"post": ["porn"]}, + "labels": { "post": ["porn"] }, "behaviors": { - "content": {"cause": "label:porn", "filter": true}, - "embed": {"cause": "label:porn", "blur": true} + "content": { "cause": "label:porn", "filter": true }, + "embed": { "cause": "label:porn", "blur": true } } }, "Blur-media label ('porn') on author profile (hide)": { "cfg": "porn-hide", "subject": "post", "author": "alice", - "labels": {"profile": ["porn"]}, + "labels": { "profile": ["porn"] }, "behaviors": { - "avatar": {"blur": true} + "avatar": { "blur": true } } }, "Blur-media label ('porn') on author account (hide)": { "cfg": "porn-hide", "subject": "post", "author": "alice", - "labels": {"account": ["porn"]}, + "labels": { "account": ["porn"] }, "behaviors": { - "content": {"cause": "label:porn", "filter": true}, - "avatar": {"blur": true}, - "embed": {"cause": "label:porn", "blur": true} + "content": { "cause": "label:porn", "filter": true }, + "avatar": { "blur": true }, + "embed": { "cause": "label:porn", "blur": true } } }, "Blur-media label ('porn') on quoted post (hide)": { @@ -377,10 +425,10 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedPost": ["porn"]}, + "labels": { "quotedPost": ["porn"] }, "behaviors": { - "content": {"cause": "label:porn", "filter": true}, - "embed": {"cause": "label:porn", "blur": true} + "content": { "cause": "label:porn", "filter": true }, + "embed": { "cause": "label:porn", "blur": true } } }, "Blur-media label ('porn') on quoted author account (hide)": { @@ -388,9 +436,9 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedAccount": ["porn"]}, + "labels": { "quotedAccount": ["porn"] }, "behaviors": { - "content": {"cause": "label:porn", "filter": true} + "content": { "cause": "label:porn", "filter": true } } }, @@ -398,28 +446,28 @@ "cfg": "porn-warn", "subject": "post", "author": "alice", - "labels": {"post": ["porn"]}, + "labels": { "post": ["porn"] }, "behaviors": { - "embed": {"cause": "label:porn", "blur": true} + "embed": { "cause": "label:porn", "blur": true } } }, "Blur-media label ('porn') on author profile (warn)": { "cfg": "porn-warn", "subject": "post", "author": "alice", - "labels": {"profile": ["porn"]}, + "labels": { "profile": ["porn"] }, "behaviors": { - "avatar": {"blur": true} + "avatar": { "blur": true } } }, "Blur-media label ('porn') on author account (warn)": { "cfg": "porn-warn", "subject": "post", "author": "alice", - "labels": {"account": ["porn"]}, + "labels": { "account": ["porn"] }, "behaviors": { - "avatar": {"blur": true}, - "embed": {"cause": "label:porn", "blur": true} + "avatar": { "blur": true }, + "embed": { "cause": "label:porn", "blur": true } } }, "Blur-media label ('porn') on quoted post (warn)": { @@ -427,9 +475,9 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedPost": ["porn"]}, + "labels": { "quotedPost": ["porn"] }, "behaviors": { - "embed": {"cause": "label:porn", "blur": true} + "embed": { "cause": "label:porn", "blur": true } } }, "Blur-media label ('porn') on quoted author account (warn)": { @@ -437,30 +485,29 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedAccount": ["porn"]}, - "behaviors": { - } + "labels": { "quotedAccount": ["porn"] }, + "behaviors": {} }, "Blur-media label ('porn') on post (ignore)": { "cfg": "porn-ignore", "subject": "post", "author": "alice", - "labels": {"post": ["porn"]}, + "labels": { "post": ["porn"] }, "behaviors": {} }, "Blur-media label ('porn') on author profile (ignore)": { "cfg": "porn-ignore", "subject": "post", "author": "alice", - "labels": {"profile": ["porn"]}, + "labels": { "profile": ["porn"] }, "behaviors": {} }, "Blur-media label ('porn') on author account (ignore)": { "cfg": "porn-ignore", "subject": "post", "author": "alice", - "labels": {"account": ["porn"]}, + "labels": { "account": ["porn"] }, "behaviors": {} }, "Blur-media label ('porn') on quoted post (ignore)": { @@ -468,7 +515,7 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedPost": ["porn"]}, + "labels": { "quotedPost": ["porn"] }, "behaviors": {} }, "Blur-media label ('porn') on quoted author account (ignore)": { @@ -476,7 +523,7 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedAccount": ["porn"]}, + "labels": { "quotedAccount": ["porn"] }, "behaviors": {} }, @@ -484,28 +531,28 @@ "cfg": "scam-hide", "subject": "post", "author": "alice", - "labels": {"post": ["scam"]}, + "labels": { "post": ["scam"] }, "behaviors": { - "content": {"cause": "label:scam", "filter": true, "alert": true} + "content": { "cause": "label:scam", "filter": true, "alert": true } } }, "Notice label ('scam') on author profile (hide)": { "cfg": "scam-hide", "subject": "post", "author": "alice", - "labels": {"profile": ["scam"]}, + "labels": { "profile": ["scam"] }, "behaviors": { - "avatar": {"alert": true} + "avatar": { "alert": true } } }, "Notice label ('scam') on author account (hide)": { "cfg": "scam-hide", "subject": "post", "author": "alice", - "labels": {"account": ["scam"]}, + "labels": { "account": ["scam"] }, "behaviors": { - "content": {"cause": "label:scam", "filter": true, "alert": true}, - "avatar": {"alert": true} + "content": { "cause": "label:scam", "filter": true, "alert": true }, + "avatar": { "alert": true } } }, "Notice label ('scam') on quoted post (hide)": { @@ -513,10 +560,10 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedPost": ["scam"]}, + "labels": { "quotedPost": ["scam"] }, "behaviors": { - "content": {"cause": "label:scam", "filter": true}, - "embed": {"cause": "label:scam", "alert": true} + "content": { "cause": "label:scam", "filter": true }, + "embed": { "cause": "label:scam", "alert": true } } }, "Notice label ('scam') on quoted author account (hide)": { @@ -524,10 +571,10 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedAccount": ["scam"]}, + "labels": { "quotedAccount": ["scam"] }, "behaviors": { - "content": {"cause": "label:scam", "filter": true}, - "embed": {"cause": "label:scam", "alert": true} + "content": { "cause": "label:scam", "filter": true }, + "embed": { "cause": "label:scam", "alert": true } } }, @@ -535,28 +582,28 @@ "cfg": "scam-warn", "subject": "post", "author": "alice", - "labels": {"post": ["scam"]}, + "labels": { "post": ["scam"] }, "behaviors": { - "content": {"cause": "label:scam", "alert": true} + "content": { "cause": "label:scam", "alert": true } } }, "Notice label ('scam') on author profile (warn)": { "cfg": "scam-warn", "subject": "post", "author": "alice", - "labels": {"profile": ["scam"]}, + "labels": { "profile": ["scam"] }, "behaviors": { - "avatar": {"alert": true} + "avatar": { "alert": true } } }, "Notice label ('scam') on author account (warn)": { "cfg": "scam-warn", "subject": "post", "author": "alice", - "labels": {"account": ["scam"]}, + "labels": { "account": ["scam"] }, "behaviors": { - "content": {"cause": "label:scam", "alert": true}, - "avatar": {"alert": true} + "content": { "cause": "label:scam", "alert": true }, + "avatar": { "alert": true } } }, "Notice label ('scam') on quoted post (warn)": { @@ -564,9 +611,9 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedPost": ["scam"]}, + "labels": { "quotedPost": ["scam"] }, "behaviors": { - "embed": {"cause": "label:scam", "alert": true} + "embed": { "cause": "label:scam", "alert": true } } }, "Notice label ('scam') on quoted author account (warn)": { @@ -574,9 +621,9 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedAccount": ["scam"]}, + "labels": { "quotedAccount": ["scam"] }, "behaviors": { - "embed": {"cause": "label:scam", "alert": true} + "embed": { "cause": "label:scam", "alert": true } } }, @@ -584,21 +631,21 @@ "cfg": "scam-ignore", "subject": "post", "author": "alice", - "labels": {"post": ["scam"]}, + "labels": { "post": ["scam"] }, "behaviors": {} }, "Notice label ('scam') on author profile (ignore)": { "cfg": "scam-ignore", "subject": "post", "author": "alice", - "labels": {"profile": ["scam"]}, + "labels": { "profile": ["scam"] }, "behaviors": {} }, "Notice label ('scam') on author account (ignore)": { "cfg": "scam-ignore", "subject": "post", "author": "alice", - "labels": {"account": ["scam"]}, + "labels": { "account": ["scam"] }, "behaviors": {} }, "Notice label ('scam') on quoted post (ignore)": { @@ -606,7 +653,7 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedPost": ["scam"]}, + "labels": { "quotedPost": ["scam"] }, "behaviors": {} }, "Notice label ('scam') on quoted author account (ignore)": { @@ -614,7 +661,7 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedAccount": ["scam"]}, + "labels": { "quotedAccount": ["scam"] }, "behaviors": {} }, @@ -622,30 +669,38 @@ "cfg": "adult-disabled", "subject": "post", "author": "alice", - "labels": {"post": ["porn"]}, + "labels": { "post": ["porn"] }, "behaviors": { - "content": {"cause": "label:porn", "filter": true, "noOverride": true}, - "embed": {"cause": "label:porn", "blur": true, "noOverride": true} + "content": { + "cause": "label:porn", + "filter": true, + "noOverride": true + }, + "embed": { "cause": "label:porn", "blur": true, "noOverride": true } } }, "Adult-only label on author profile when adult content is disabled": { "cfg": "adult-disabled", "subject": "post", "author": "alice", - "labels": {"profile": ["porn"]}, + "labels": { "profile": ["porn"] }, "behaviors": { - "avatar": {"cause": "label:porn", "blur": true, "noOverride": true} + "avatar": { "cause": "label:porn", "blur": true, "noOverride": true } } }, "Adult-only label on author account when adult content is disabled": { "cfg": "adult-disabled", "subject": "post", "author": "alice", - "labels": {"account": ["porn"]}, + "labels": { "account": ["porn"] }, "behaviors": { - "content": {"cause": "label:porn", "filter": true, "noOverride": true}, - "avatar": {"cause": "label:porn", "blur": true, "noOverride": true}, - "embed": {"cause": "label:porn", "blur": true, "noOverride": true} + "content": { + "cause": "label:porn", + "filter": true, + "noOverride": true + }, + "avatar": { "cause": "label:porn", "blur": true, "noOverride": true }, + "embed": { "cause": "label:porn", "blur": true, "noOverride": true } } }, "Adult-only label on quoted post when adult content is disabled": { @@ -653,10 +708,10 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedPost": ["porn"]}, + "labels": { "quotedPost": ["porn"] }, "behaviors": { - "content": {"cause": "label:porn", "filter": true}, - "embed": {"cause": "label:porn", "blur": true, "noOverride": true} + "content": { "cause": "label:porn", "filter": true }, + "embed": { "cause": "label:porn", "blur": true, "noOverride": true } } }, "Adult-only label on quoted author account when adult content is disabled": { @@ -664,9 +719,9 @@ "subject": "post", "author": "alice", "quoteAuthor": "alice", - "labels": {"quotedAccount": ["porn"]}, + "labels": { "quotedAccount": ["porn"] }, "behaviors": { - "content": {"cause": "label:porn", "filter": true} + "content": { "cause": "label:porn", "filter": true } } }, @@ -674,23 +729,23 @@ "cfg": "none", "subject": "post", "author": "self", - "labels": {"post": ["!hide"]}, + "labels": { "post": ["!hide"] }, "behaviors": { - "content": {"cause": "label:!hide", "blur": true} + "content": { "cause": "label:!hide", "blur": true } } }, "Self-post: Imperative label ('!hide') on author profile": { "cfg": "none", "subject": "post", "author": "self", - "labels": {"profile": ["!hide"]}, + "labels": { "profile": ["!hide"] }, "behaviors": {} }, "Self-post: Imperative label ('!hide') on author account": { "cfg": "none", "subject": "post", "author": "self", - "labels": {"account": ["!hide"]}, + "labels": { "account": ["!hide"] }, "behaviors": {} }, "Self-post: Imperative label ('!hide') on quoted post": { @@ -698,9 +753,9 @@ "subject": "post", "author": "self", "quoteAuthor": "self", - "labels": {"quotedPost": ["!hide"]}, + "labels": { "quotedPost": ["!hide"] }, "behaviors": { - "embed": {"cause": "label:!hide", "blur": true} + "embed": { "cause": "label:!hide", "blur": true } } }, "Self-post: Imperative label ('!hide') on quoted author account": { @@ -708,7 +763,7 @@ "subject": "post", "author": "self", "quoteAuthor": "self", - "labels": {"quotedAccount": ["!hide"]}, + "labels": { "quotedAccount": ["!hide"] }, "behaviors": {} }, @@ -716,23 +771,23 @@ "cfg": "none", "subject": "post", "author": "self", - "labels": {"post": ["!warn"]}, + "labels": { "post": ["!warn"] }, "behaviors": { - "content": {"cause": "label:!warn", "blur": true} + "content": { "cause": "label:!warn", "blur": true } } }, "Self-post: Imperative label ('!warn') on author profile": { "cfg": "none", "subject": "post", "author": "self", - "labels": {"profile": ["!warn"]}, + "labels": { "profile": ["!warn"] }, "behaviors": {} }, "Self-post: Imperative label ('!warn') on author account": { "cfg": "none", "subject": "post", "author": "self", - "labels": {"account": ["!warn"]}, + "labels": { "account": ["!warn"] }, "behaviors": {} }, "Self-post: Imperative label ('!warn') on quoted post": { @@ -740,9 +795,9 @@ "subject": "post", "author": "self", "quoteAuthor": "self", - "labels": {"quotedPost": ["!warn"]}, + "labels": { "quotedPost": ["!warn"] }, "behaviors": { - "embed": {"cause": "label:!warn", "blur": true} + "embed": { "cause": "label:!warn", "blur": true } } }, "Self-post: Imperative label ('!warn') on quoted author account": { @@ -750,7 +805,7 @@ "subject": "post", "author": "self", "quoteAuthor": "self", - "labels": {"quotedAccount": ["!warn"]}, + "labels": { "quotedAccount": ["!warn"] }, "behaviors": {} }, @@ -758,35 +813,33 @@ "cfg": "porn-hide", "subject": "post", "author": "self", - "labels": {"post": ["porn"]}, + "labels": { "post": ["porn"] }, "behaviors": { - "embed": {"cause": "label:porn", "blur": true} + "embed": { "cause": "label:porn", "blur": true } } }, "Self-post: Blur-media label ('porn') on author profile (hide)": { "cfg": "porn-hide", "subject": "post", "author": "self", - "labels": {"profile": ["porn"]}, - "behaviors": { - } + "labels": { "profile": ["porn"] }, + "behaviors": {} }, "Self-post: Blur-media label ('porn') on author account (hide)": { "cfg": "porn-hide", "subject": "post", "author": "self", - "labels": {"account": ["porn"]}, - "behaviors": { - } + "labels": { "account": ["porn"] }, + "behaviors": {} }, "Self-post: Blur-media label ('porn') on quoted post (hide)": { "cfg": "porn-hide", "subject": "post", "author": "self", "quoteAuthor": "self", - "labels": {"quotedPost": ["porn"]}, + "labels": { "quotedPost": ["porn"] }, "behaviors": { - "embed": {"cause": "label:porn", "blur": true} + "embed": { "cause": "label:porn", "blur": true } } }, "Self-post: Blur-media label ('porn') on quoted author account (hide)": { @@ -794,44 +847,41 @@ "subject": "post", "author": "self", "quoteAuthor": "self", - "labels": {"quotedAccount": ["porn"]}, - "behaviors": { - } + "labels": { "quotedAccount": ["porn"] }, + "behaviors": {} }, "Self-post: Blur-media label ('porn') on post (warn)": { "cfg": "porn-warn", "subject": "post", "author": "self", - "labels": {"post": ["porn"]}, + "labels": { "post": ["porn"] }, "behaviors": { - "embed": {"cause": "label:porn", "blur": true} + "embed": { "cause": "label:porn", "blur": true } } }, "Self-post: Blur-media label ('porn') on author profile (warn)": { "cfg": "porn-warn", "subject": "post", "author": "self", - "labels": {"profile": ["porn"]}, - "behaviors": { - } + "labels": { "profile": ["porn"] }, + "behaviors": {} }, "Self-post: Blur-media label ('porn') on author account (warn)": { "cfg": "porn-warn", "subject": "post", "author": "self", - "labels": {"account": ["porn"]}, - "behaviors": { - } + "labels": { "account": ["porn"] }, + "behaviors": {} }, "Self-post: Blur-media label ('porn') on quoted post (warn)": { "cfg": "porn-warn", "subject": "post", "author": "self", "quoteAuthor": "self", - "labels": {"quotedPost": ["porn"]}, + "labels": { "quotedPost": ["porn"] }, "behaviors": { - "embed": {"cause": "label:porn", "blur": true} + "embed": { "cause": "label:porn", "blur": true } } }, "Self-post: Blur-media label ('porn') on quoted author account (warn)": { @@ -839,9 +889,8 @@ "subject": "post", "author": "self", "quoteAuthor": "self", - "labels": {"quotedAccount": ["porn"]}, - "behaviors": { - } + "labels": { "quotedAccount": ["porn"] }, + "behaviors": {} }, "Post with blocked author": { @@ -850,8 +899,13 @@ "author": "bob", "labels": {}, "behaviors": { - "content": {"cause": "blocking", "filter": true, "blur": true, "noOverride": true}, - "avatar": {"blur": true, "noOverride": true} + "content": { + "cause": "blocking", + "filter": true, + "blur": true, + "noOverride": true + }, + "avatar": { "blur": true, "noOverride": true } } }, "Post with blocked quoted author": { @@ -861,8 +915,8 @@ "quoteAuthor": "bob", "labels": {}, "behaviors": { - "content": {"cause": "blocking", "filter": true}, - "embed": {"cause": "blocking", "blur": true, "noOverride": true} + "content": { "cause": "blocking", "filter": true }, + "embed": { "cause": "blocking", "blur": true, "noOverride": true } } }, @@ -872,8 +926,13 @@ "author": "carla", "labels": {}, "behaviors": { - "content": {"cause": "blocked-by", "filter": true, "blur": true, "noOverride": true}, - "avatar": {"blur": true, "noOverride": true} + "content": { + "cause": "blocked-by", + "filter": true, + "blur": true, + "noOverride": true + }, + "avatar": { "blur": true, "noOverride": true } } }, "Post with quoted author blocking user": { @@ -883,8 +942,8 @@ "quoteAuthor": "carla", "labels": {}, "behaviors": { - "content": {"cause": "blocked-by", "filter": true}, - "embed": {"cause": "blocked-by", "blur": true, "noOverride": true} + "content": { "cause": "blocked-by", "filter": true }, + "embed": { "cause": "blocked-by", "blur": true, "noOverride": true } } }, @@ -894,7 +953,7 @@ "author": "dan", "labels": {}, "behaviors": { - "content": {"cause": "muted", "filter": true, "blur": true} + "content": { "cause": "muted", "filter": true, "blur": true } } }, "Post with muted quoted author": { @@ -904,8 +963,8 @@ "quoteAuthor": "dan", "labels": {}, "behaviors": { - "content": {"cause": "muted", "filter": true}, - "embed": {"cause": "muted", "blur": true} + "content": { "cause": "muted", "filter": true }, + "embed": { "cause": "muted", "blur": true } } }, @@ -915,7 +974,7 @@ "author": "elise", "labels": {}, "behaviors": { - "content": {"cause": "muted-by-list", "filter": true, "blur": true} + "content": { "cause": "muted-by-list", "filter": true, "blur": true } } }, "Post with muted-by-list quoted author": { @@ -925,8 +984,8 @@ "quoteAuthor": "elise", "labels": {}, "behaviors": { - "content": {"cause": "muted-by-list", "filter": true}, - "embed": {"cause": "muted-by-list", "blur": true} + "content": { "cause": "muted-by-list", "filter": true }, + "embed": { "cause": "muted-by-list", "blur": true } } }, @@ -936,8 +995,13 @@ "author": "fern", "labels": {}, "behaviors": { - "content": {"cause": "blocking", "filter": true, "blur": true, "noOverride": true}, - "avatar": {"blur": true, "noOverride": true} + "content": { + "cause": "blocking", + "filter": true, + "blur": true, + "noOverride": true + }, + "avatar": { "blur": true, "noOverride": true } } }, "Prioritization: post with blocking & blocked-by quoted author": { @@ -947,18 +1011,23 @@ "quoteAuthor": "fern", "labels": {}, "behaviors": { - "content": {"cause": "blocking", "filter": true}, - "embed": {"cause": "blocking", "blur": true, "noOverride": true} + "content": { "cause": "blocking", "filter": true }, + "embed": { "cause": "blocking", "blur": true, "noOverride": true } } }, "Prioritization: '!hide' label on post by blocked user": { "cfg": "none", "subject": "post", "author": "bob", - "labels": {"post": ["!hide"]}, + "labels": { "post": ["!hide"] }, "behaviors": { - "content": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true}, - "avatar": {"blur": true, "noOverride": true} + "content": { + "cause": "label:!hide", + "filter": true, + "blur": true, + "noOverride": true + }, + "avatar": { "blur": true, "noOverride": true } } }, "Prioritization: '!hide' label on quoted post, post by blocked user": { @@ -966,49 +1035,64 @@ "subject": "post", "author": "bob", "quoteAuthor": "alice", - "labels": {"quotedPost": ["!hide"]}, + "labels": { "quotedPost": ["!hide"] }, "behaviors": { - "content": {"cause": "blocking", "filter": true, "blur": true, "noOverride": true}, - "avatar": {"blur": true, "noOverride": true}, - "embed": {"cause": "label:!hide", "blur": true, "noOverride": true} + "content": { + "cause": "blocking", + "filter": true, + "blur": true, + "noOverride": true + }, + "avatar": { "blur": true, "noOverride": true }, + "embed": { "cause": "label:!hide", "blur": true, "noOverride": true } } }, "Prioritization: '!hide' and 'intolerant' labels on post (hide)": { "cfg": "intolerant-hide", "subject": "post", "author": "alice", - "labels": {"post": ["!hide", "intolerant"]}, + "labels": { "post": ["!hide", "intolerant"] }, "behaviors": { - "content": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true} + "content": { + "cause": "label:!hide", + "filter": true, + "blur": true, + "noOverride": true + } } }, "Prioritization: '!warn' and 'intolerant' labels on post (hide)": { "cfg": "intolerant-hide", "subject": "post", "author": "alice", - "labels": {"post": ["!warn", "intolerant"]}, + "labels": { "post": ["!warn", "intolerant"] }, "behaviors": { - "content": {"cause": "label:intolerant", "filter": true, "blur": true} + "content": { "cause": "label:intolerant", "filter": true, "blur": true } } }, "Prioritization: '!hide' and 'porn' labels on post (hide)": { "cfg": "porn-hide", "subject": "post", "author": "alice", - "labels": {"post": ["!hide", "porn"]}, + "labels": { "post": ["!hide", "porn"] }, "behaviors": { - "content": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true} + "content": { + "cause": "label:!hide", + "filter": true, + "blur": true, + "noOverride": true + } } }, "Prioritization: '!warn' and 'porn' labels on post (hide)": { "cfg": "porn-hide", "subject": "post", "author": "alice", - "labels": {"post": ["!warn", "porn"]}, + "labels": { "post": ["!warn", "porn"] }, "behaviors": { - "content": {"cause": "label:porn", "filter": true}, - "embed": {"cause": "label:porn", "blur": true} + "content": { "cause": "label:porn", "filter": true }, + "embed": { "cause": "label:porn", "blur": true } } } } -} \ No newline at end of file +} diff --git a/packages/api/definitions/profile-moderation-behaviors.json b/packages/api/definitions/profile-moderation-behaviors.json index 7db48d8b679..52e04761618 100644 --- a/packages/api/definitions/profile-moderation-behaviors.json +++ b/packages/api/definitions/profile-moderation-behaviors.json @@ -1,13 +1,47 @@ - { "users": { - "self": {"blocking": false, "blockedBy": false, "muted": false, "mutedByList": false}, - "alice": {"blocking": false, "blockedBy": false, "muted": false, "mutedByList": false}, - "bob": {"blocking": true, "blockedBy": false, "muted": false, "mutedByList": false}, - "carla": {"blocking": false, "blockedBy": true, "muted": false, "mutedByList": false}, - "dan": {"blocking": false, "blockedBy": false, "muted": true, "mutedByList": false}, - "elise": {"blocking": false, "blockedBy": false, "muted": false, "mutedByList": true}, - "fern": {"blocking": true, "blockedBy": true, "muted": false, "mutedByList": false} + "self": { + "blocking": false, + "blockedBy": false, + "muted": false, + "mutedByList": false + }, + "alice": { + "blocking": false, + "blockedBy": false, + "muted": false, + "mutedByList": false + }, + "bob": { + "blocking": true, + "blockedBy": false, + "muted": false, + "mutedByList": false + }, + "carla": { + "blocking": false, + "blockedBy": true, + "muted": false, + "mutedByList": false + }, + "dan": { + "blocking": false, + "blockedBy": false, + "muted": true, + "mutedByList": false + }, + "elise": { + "blocking": false, + "blockedBy": false, + "muted": false, + "mutedByList": true + }, + "fern": { + "blocking": true, + "blockedBy": true, + "muted": false, + "mutedByList": false + } }, "configurations": { "none": {}, @@ -15,37 +49,37 @@ "adultContentEnabled": false }, "intolerant-hide": { - "settings": {"intolerant": "hide"} + "settings": { "intolerant": "hide" } }, "intolerant-warn": { - "settings": {"intolerant": "warn"} + "settings": { "intolerant": "warn" } }, "intolerant-ignore": { - "settings": {"intolerant": "ignore"} + "settings": { "intolerant": "ignore" } }, "porn-hide": { "adultContentEnabled": true, - "settings": {"porn": "hide"} + "settings": { "porn": "hide" } }, "porn-warn": { "adultContentEnabled": true, - "settings": {"porn": "warn"} + "settings": { "porn": "warn" } }, "porn-ignore": { "adultContentEnabled": true, - "settings": {"porn": "ignore"} + "settings": { "porn": "ignore" } }, "scam-hide": { - "settings": {"scam": "hide"} + "settings": { "scam": "hide" } }, "scam-warn": { - "settings": {"scam": "warn"} + "settings": { "scam": "warn" } }, "scam-ignore": { - "settings": {"scam": "ignore"} + "settings": { "scam": "ignore" } }, "intolerant-hide-scam-warn": { - "settings": {"intolerant": "hide", "scam": "hide"} + "settings": { "intolerant": "hide", "scam": "hide" } } }, "scenarios": { @@ -53,20 +87,25 @@ "cfg": "none", "subject": "profile", "author": "alice", - "labels": {"account": ["!hide"]}, + "labels": { "account": ["!hide"] }, "behaviors": { - "account": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true}, - "avatar": {"blur": true, "noOverride": true} + "account": { + "cause": "label:!hide", + "filter": true, + "blur": true, + "noOverride": true + }, + "avatar": { "blur": true, "noOverride": true } } }, "Imperative label ('!hide') on profile": { "cfg": "none", "subject": "profile", "author": "alice", - "labels": {"profile": ["!hide"]}, + "labels": { "profile": ["!hide"] }, "behaviors": { - "profile": {"cause": "label:!hide", "blur": true, "noOverride": true}, - "avatar": {"blur": true, "noOverride": true} + "profile": { "cause": "label:!hide", "blur": true, "noOverride": true }, + "avatar": { "blur": true, "noOverride": true } } }, @@ -74,16 +113,16 @@ "cfg": "none", "subject": "profile", "author": "alice", - "labels": {"account": ["!no-promote"]}, + "labels": { "account": ["!no-promote"] }, "behaviors": { - "account": {"cause": "label:!no-promote", "filter": true} + "account": { "cause": "label:!no-promote", "filter": true } } }, "Imperative label ('!no-promote') on profile": { "cfg": "none", "subject": "profile", "author": "alice", - "labels": {"profile": ["!no-promote"]}, + "labels": { "profile": ["!no-promote"] }, "behaviors": {} }, @@ -91,20 +130,20 @@ "cfg": "none", "subject": "profile", "author": "alice", - "labels": {"account": ["!warn"]}, + "labels": { "account": ["!warn"] }, "behaviors": { - "account": {"cause": "label:!warn", "blur": true}, - "avatar": {"blur": true} + "account": { "cause": "label:!warn", "blur": true }, + "avatar": { "blur": true } } }, "Imperative label ('!warn') on profile": { "cfg": "none", "subject": "profile", "author": "alice", - "labels": {"profile": ["!warn"]}, + "labels": { "profile": ["!warn"] }, "behaviors": { - "profile": {"cause": "label:!warn", "blur": true}, - "avatar": {"blur": true} + "profile": { "cause": "label:!warn", "blur": true }, + "avatar": { "blur": true } } }, @@ -112,20 +151,24 @@ "cfg": "intolerant-hide", "subject": "profile", "author": "alice", - "labels": {"account": ["intolerant"]}, + "labels": { "account": ["intolerant"] }, "behaviors": { - "account": {"cause": "label:intolerant", "filter": true, "blur": true}, - "avatar": {"blur": true} + "account": { + "cause": "label:intolerant", + "filter": true, + "blur": true + }, + "avatar": { "blur": true } } }, "Blur label ('intolerant') on profile (hide)": { "cfg": "intolerant-hide", "subject": "profile", "author": "alice", - "labels": {"profile": ["intolerant"]}, + "labels": { "profile": ["intolerant"] }, "behaviors": { - "profile": {"cause": "label:intolerant", "blur": true}, - "avatar": {"blur": true} + "profile": { "cause": "label:intolerant", "blur": true }, + "avatar": { "blur": true } } }, @@ -133,20 +176,20 @@ "cfg": "intolerant-warn", "subject": "profile", "author": "alice", - "labels": {"account": ["intolerant"]}, + "labels": { "account": ["intolerant"] }, "behaviors": { - "account": {"cause": "label:intolerant", "blur": true}, - "avatar": {"blur": true} + "account": { "cause": "label:intolerant", "blur": true }, + "avatar": { "blur": true } } }, "Blur label ('intolerant') on profile (warn)": { "cfg": "intolerant-warn", "subject": "profile", "author": "alice", - "labels": {"profile": ["intolerant"]}, + "labels": { "profile": ["intolerant"] }, "behaviors": { - "profile": {"cause": "label:intolerant", "blur": true}, - "avatar": {"blur": true} + "profile": { "cause": "label:intolerant", "blur": true }, + "avatar": { "blur": true } } }, @@ -154,14 +197,14 @@ "cfg": "intolerant-ignore", "subject": "profile", "author": "alice", - "labels": {"account": ["intolerant"]}, + "labels": { "account": ["intolerant"] }, "behaviors": {} }, "Blur label ('intolerant') on profile (ignore)": { "cfg": "intolerant-ignore", "subject": "profile", "author": "alice", - "labels": {"profile": ["intolerant"]}, + "labels": { "profile": ["intolerant"] }, "behaviors": {} }, @@ -169,19 +212,19 @@ "cfg": "porn-hide", "subject": "profile", "author": "alice", - "labels": {"account": ["porn"]}, + "labels": { "account": ["porn"] }, "behaviors": { - "account": {"cause": "label:porn", "filter": true, "blur": true}, - "avatar": {"blur": true} + "account": { "cause": "label:porn", "filter": true, "blur": true }, + "avatar": { "blur": true } } }, "Blur-media label ('porn') on profile (hide)": { "cfg": "porn-hide", "subject": "profile", "author": "alice", - "labels": {"profile": ["porn"]}, + "labels": { "profile": ["porn"] }, "behaviors": { - "avatar": {"blur": true} + "avatar": { "blur": true } } }, @@ -189,19 +232,19 @@ "cfg": "porn-warn", "subject": "profile", "author": "alice", - "labels": {"account": ["porn"]}, + "labels": { "account": ["porn"] }, "behaviors": { - "account": {"cause": "label:porn", "blur": true}, - "avatar": {"blur": true} + "account": { "cause": "label:porn", "blur": true }, + "avatar": { "blur": true } } }, "Blur-media label ('porn') on profile (warn)": { "cfg": "porn-warn", "subject": "profile", "author": "alice", - "labels": {"profile": ["porn"]}, + "labels": { "profile": ["porn"] }, "behaviors": { - "avatar": {"blur": true} + "avatar": { "blur": true } } }, @@ -209,14 +252,14 @@ "cfg": "porn-ignore", "subject": "profile", "author": "alice", - "labels": {"account": ["porn"]}, + "labels": { "account": ["porn"] }, "behaviors": {} }, "Blur-media label ('porn') on profile (ignore)": { "cfg": "porn-ignore", "subject": "profile", "author": "alice", - "labels": {"profile": ["porn"]}, + "labels": { "profile": ["porn"] }, "behaviors": {} }, @@ -224,20 +267,20 @@ "cfg": "scam-hide", "subject": "profile", "author": "alice", - "labels": {"account": ["scam"]}, + "labels": { "account": ["scam"] }, "behaviors": { - "account": {"cause": "label:scam", "filter": true, "alert": true}, - "avatar": {"alert": true} + "account": { "cause": "label:scam", "filter": true, "alert": true }, + "avatar": { "alert": true } } }, "Notice label ('scam') on profile (hide)": { "cfg": "scam-hide", "subject": "profile", "author": "alice", - "labels": {"profile": ["scam"]}, + "labels": { "profile": ["scam"] }, "behaviors": { - "profile": {"cause": "label:scam", "alert": true}, - "avatar": {"alert": true} + "profile": { "cause": "label:scam", "alert": true }, + "avatar": { "alert": true } } }, @@ -245,20 +288,20 @@ "cfg": "scam-warn", "subject": "profile", "author": "alice", - "labels": {"account": ["scam"]}, + "labels": { "account": ["scam"] }, "behaviors": { - "account": {"cause": "label:scam", "alert": true}, - "avatar": {"alert": true} + "account": { "cause": "label:scam", "alert": true }, + "avatar": { "alert": true } } }, "Notice label ('scam') on profile (warn)": { "cfg": "scam-warn", "subject": "profile", "author": "alice", - "labels": {"profile": ["scam"]}, + "labels": { "profile": ["scam"] }, "behaviors": { - "profile": {"cause": "label:scam", "alert": true}, - "avatar": {"alert": true} + "profile": { "cause": "label:scam", "alert": true }, + "avatar": { "alert": true } } }, @@ -266,14 +309,14 @@ "cfg": "scam-ignore", "subject": "profile", "author": "alice", - "labels": {"account": ["scam"]}, + "labels": { "account": ["scam"] }, "behaviors": {} }, "Notice label ('scam') on profile (ignore)": { "cfg": "scam-ignore", "subject": "profile", "author": "alice", - "labels": {"profile": ["scam"]}, + "labels": { "profile": ["scam"] }, "behaviors": {} }, @@ -281,19 +324,24 @@ "cfg": "adult-disabled", "subject": "profile", "author": "alice", - "labels": {"account": ["porn"]}, + "labels": { "account": ["porn"] }, "behaviors": { - "account": {"cause": "label:porn", "filter": true, "blur": true, "noOverride": true}, - "avatar": {"blur": true, "noOverride": true} + "account": { + "cause": "label:porn", + "filter": true, + "blur": true, + "noOverride": true + }, + "avatar": { "blur": true, "noOverride": true } } }, "Adult-only label on profile when adult content is disabled": { "cfg": "adult-disabled", "subject": "profile", "author": "alice", - "labels": {"profile": ["porn"]}, + "labels": { "profile": ["porn"] }, "behaviors": { - "avatar": {"blur": true, "noOverride": true} + "avatar": { "blur": true, "noOverride": true } } }, @@ -301,20 +349,20 @@ "cfg": "none", "subject": "profile", "author": "self", - "labels": {"account": ["!hide"]}, + "labels": { "account": ["!hide"] }, "behaviors": { - "account": {"cause": "label:!hide", "alert": true}, - "avatar": {"alert": true} + "account": { "cause": "label:!hide", "alert": true }, + "avatar": { "alert": true } } }, "Self-profile: !hide on profile": { "cfg": "none", "subject": "profile", "author": "self", - "labels": {"profile": ["!hide"]}, + "labels": { "profile": ["!hide"] }, "behaviors": { - "profile": {"cause": "label:!hide", "alert": true}, - "avatar": {"alert": true} + "profile": { "cause": "label:!hide", "alert": true }, + "avatar": { "alert": true } } }, @@ -324,8 +372,8 @@ "author": "bob", "labels": {}, "behaviors": { - "account": {"cause": "blocking", "filter": true}, - "avatar": {"blur": true, "noOverride": true} + "account": { "cause": "blocking", "filter": true }, + "avatar": { "blur": true, "noOverride": true } } }, @@ -335,8 +383,8 @@ "author": "carla", "labels": {}, "behaviors": { - "account": {"cause": "blocked-by", "filter": true}, - "avatar": {"blur": true, "noOverride": true} + "account": { "cause": "blocked-by", "filter": true }, + "avatar": { "blur": true, "noOverride": true } } }, @@ -346,7 +394,7 @@ "author": "dan", "labels": {}, "behaviors": { - "account": {"cause": "muted", "filter": true} + "account": { "cause": "muted", "filter": true } } }, @@ -356,7 +404,7 @@ "author": "elise", "labels": {}, "behaviors": { - "account": {"cause": "muted-by-list", "filter": true} + "account": { "cause": "muted-by-list", "filter": true } } }, @@ -366,82 +414,105 @@ "author": "fern", "labels": {}, "behaviors": { - "account": {"cause": "blocking", "filter": true, "blur": false}, - "avatar": {"blur": true, "noOverride": true} + "account": { "cause": "blocking", "filter": true, "blur": false }, + "avatar": { "blur": true, "noOverride": true } } }, "Prioritization: '!hide' label on account of blocked user": { "cfg": "none", "subject": "profile", "author": "bob", - "labels": {"account": ["!hide"]}, + "labels": { "account": ["!hide"] }, "behaviors": { - "account": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true}, - "avatar": {"blur": true, "noOverride": true} + "account": { + "cause": "label:!hide", + "filter": true, + "blur": true, + "noOverride": true + }, + "avatar": { "blur": true, "noOverride": true } } }, "Prioritization: '!hide' and 'intolerant' labels on account (hide)": { "cfg": "intolerant-hide", "subject": "profile", "author": "alice", - "labels": {"account": ["!hide", "intolerant"]}, + "labels": { "account": ["!hide", "intolerant"] }, "behaviors": { - "account": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true}, - "avatar": {"blur": true, "noOverride": true} + "account": { + "cause": "label:!hide", + "filter": true, + "blur": true, + "noOverride": true + }, + "avatar": { "blur": true, "noOverride": true } } }, "Prioritization: '!warn' and 'intolerant' labels on account (hide)": { "cfg": "intolerant-hide", "subject": "profile", "author": "alice", - "labels": {"account": ["!warn", "intolerant"]}, + "labels": { "account": ["!warn", "intolerant"] }, "behaviors": { - "account": {"cause": "label:intolerant", "filter": true, "blur": true}, - "avatar": {"blur": true} + "account": { + "cause": "label:intolerant", + "filter": true, + "blur": true + }, + "avatar": { "blur": true } } }, "Prioritization: '!warn' and 'porn' labels on account (hide)": { "cfg": "porn-hide", "subject": "profile", "author": "alice", - "labels": {"account": ["!warn", "porn"]}, + "labels": { "account": ["!warn", "porn"] }, "behaviors": { - "account": {"cause": "label:porn", "filter": true, "blur": true}, - "avatar": {"blur": true} + "account": { "cause": "label:porn", "filter": true, "blur": true }, + "avatar": { "blur": true } } }, "Prioritization: intolerant label on account (hide) and scam label on profile (warn)": { "cfg": "intolerant-hide-scam-warn", "subject": "profile", "author": "alice", - "labels": {"account": ["intolerant"], "profile": ["scam"]}, + "labels": { "account": ["intolerant"], "profile": ["scam"] }, "behaviors": { - "account": {"cause": "label:intolerant", "filter": true, "blur": true}, - "profile": {"cause": "label:scam", "alert": true}, - "avatar": {"blur": true, "alert": true} + "account": { + "cause": "label:intolerant", + "filter": true, + "blur": true + }, + "profile": { "cause": "label:scam", "alert": true }, + "avatar": { "blur": true, "alert": true } } }, "Prioritization: !hide on account, !warn on profile": { "cfg": "none", "subject": "profile", "author": "alice", - "labels": {"account": ["!hide"], "profile": ["!warn"]}, + "labels": { "account": ["!hide"], "profile": ["!warn"] }, "behaviors": { - "account": {"cause": "label:!hide", "filter": true, "blur": true, "noOverride": true}, - "profile": {"cause": "label:!warn", "blur": true}, - "avatar": {"blur": true, "noOverride": true} + "account": { + "cause": "label:!hide", + "filter": true, + "blur": true, + "noOverride": true + }, + "profile": { "cause": "label:!warn", "blur": true }, + "avatar": { "blur": true, "noOverride": true } } }, "Prioritization: !warn on account, !hide on profile": { "cfg": "none", "subject": "profile", "author": "alice", - "labels": {"account": ["!warn"], "profile": ["!hide"]}, + "labels": { "account": ["!warn"], "profile": ["!hide"] }, "behaviors": { - "account": {"cause": "label:!warn", "blur": true}, - "profile": {"cause": "label:!hide", "blur": true, "noOverride": true}, - "avatar": {"blur": true, "noOverride": true} + "account": { "cause": "label:!warn", "blur": true }, + "profile": { "cause": "label:!hide", "blur": true, "noOverride": true }, + "avatar": { "blur": true, "noOverride": true } } } } -} \ No newline at end of file +} diff --git a/packages/api/definitions/proposed-labels.json b/packages/api/definitions/proposed-labels.json index 59cbcb4adfc..ad9b8924c8a 100644 --- a/packages/api/definitions/proposed-labels.json +++ b/packages/api/definitions/proposed-labels.json @@ -323,4 +323,4 @@ } ] } -] \ No newline at end of file +] diff --git a/packages/api/docs/labels.md b/packages/api/docs/labels.md index 9531df460c1..a2d8806b566 100644 --- a/packages/api/docs/labels.md +++ b/packages/api/docs/labels.md @@ -1,44 +1,44 @@ - # Labels - - This document is a reference for the labels used in the SDK. +# Labels - **⚠️ Note**: These labels are still in development and may change over time. Not all are currently in use. +This document is a reference for the labels used in the SDK. - ## Key +**⚠️ Note**: These labels are still in development and may change over time. Not all are currently in use. - ### Label Preferences +## Key - The possible client interpretations for a label. +### Label Preferences - - ignore Do nothing with the label. - - warn Provide some form of warning on the content (see "On Warn" behavior). - - hide Remove the content from feeds and apply the warning when directly viewed. +The possible client interpretations for a label. - Each label specifies which preferences it can support. If a label is not configurable, it must have only own supported preference. +- ignore Do nothing with the label. +- warn Provide some form of warning on the content (see "On Warn" behavior). +- hide Remove the content from feeds and apply the warning when directly viewed. - ### Configurable? +Each label specifies which preferences it can support. If a label is not configurable, it must have only own supported preference. - Non-configurable labels cannot have their preference changed by the user. +### Configurable? - ### Flags +Non-configurable labels cannot have their preference changed by the user. - Additional behaviors which a label can adopt. +### Flags - - no-override The user cannot click through any covering of content created by the label. - - adult The user must have adult content enabled to configure the label. If adult content is not enabled, the label must adopt the strictest preference. +Additional behaviors which a label can adopt. - ### On Warn +- no-override The user cannot click through any covering of content created by the label. +- adult The user must have adult content enabled to configure the label. If adult content is not enabled, the label must adopt the strictest preference. - The kind of UI behavior used when a warning must be applied. +### On Warn - - blur Hide all of the content behind an interstitial. - - blur-media Hide only the media within the content (ie images) behind an interstitial. - - alert Display a descriptive warning but do not hide the content. - - null Do nothing. +The kind of UI behavior used when a warning must be applied. - ## Label Behaviors +- blur Hide all of the content behind an interstitial. +- blur-media Hide only the media within the content (ie images) behind an interstitial. +- alert Display a descriptive warning but do not hide the content. +- null Do nothing. + +## Label Behaviors @@ -267,7 +267,7 @@
- ## Label Group Descriptions +## Label Group Descriptions @@ -312,7 +312,7 @@
- ## Label Descriptions +## Label Descriptions @@ -535,4 +535,4 @@ on content
Misleading
The moderators believe this account is spreading misleading information.

-
\ No newline at end of file + diff --git a/packages/api/docs/moderation-behaviors/posts.md b/packages/api/docs/moderation-behaviors/posts.md index ef3c6c7fc5d..5ddcf9ff602 100644 --- a/packages/api/docs/moderation-behaviors/posts.md +++ b/packages/api/docs/moderation-behaviors/posts.md @@ -38,17 +38,12 @@ Key: - - - - - Imperative label ('!hide') on author profile @@ -56,7 +51,6 @@ Key: - 🚫 @@ -64,13 +58,9 @@ Key: - - - - Imperative label ('!hide') on author account @@ -86,13 +76,9 @@ Key: - - - - Imperative label ('!hide') on quoted post @@ -100,11 +86,9 @@ Key: - - 🚫 @@ -112,9 +96,6 @@ Key: - - - Imperative label ('!hide') on quoted author account @@ -122,11 +103,9 @@ Key: - - 🚫 @@ -134,9 +113,6 @@ Key: - - - Imperative label ('!no-promote') on post @@ -144,21 +120,15 @@ Key: - - - - - - Imperative label ('!no-promote') on author profile @@ -166,21 +136,15 @@ Key: - - - - - - Imperative label ('!no-promote') on author account @@ -188,21 +152,15 @@ Key: - - - - - - Imperative label ('!no-promote') on quoted post @@ -210,21 +168,15 @@ Key: - - - - - - Imperative label ('!no-promote') on quoted author account @@ -232,21 +184,15 @@ Key: - - - - - - Imperative label ('!warn') on post @@ -258,17 +204,12 @@ Key: - - - - - Imperative label ('!warn') on author profile @@ -276,7 +217,6 @@ Key: - ✋ @@ -284,13 +224,9 @@ Key: - - - - Imperative label ('!warn') on author account @@ -306,13 +242,9 @@ Key: - - - - Imperative label ('!warn') on quoted post @@ -320,11 +252,9 @@ Key: - - ✋ @@ -332,9 +262,6 @@ Key: - - - Imperative label ('!warn') on quoted author account @@ -342,11 +269,9 @@ Key: - - ✋ @@ -354,8 +279,6 @@ Key: - - ScenarioFilterContentAvatarEmbed Blur label ('intolerant') on post (hide) @@ -368,17 +291,12 @@ Key: - - - - - Blur label ('intolerant') on author profile (hide) @@ -386,7 +304,6 @@ Key: - ✋ @@ -394,13 +311,9 @@ Key: - - - - Blur label ('intolerant') on author account (hide) @@ -416,13 +329,9 @@ Key: - - - - Blur label ('intolerant') on quoted post (hide) @@ -430,11 +339,9 @@ Key: - - ✋ @@ -442,9 +349,6 @@ Key: - - - Blur label ('intolerant') on quoted author account (hide) @@ -452,11 +356,9 @@ Key: - - ✋ @@ -464,9 +366,6 @@ Key: - - - Blur label ('intolerant') on post (warn) @@ -478,17 +377,12 @@ Key: - - - - - Blur label ('intolerant') on author profile (warn) @@ -496,7 +390,6 @@ Key: - ✋ @@ -504,13 +397,9 @@ Key: - - - - Blur label ('intolerant') on author account (warn) @@ -526,13 +415,9 @@ Key: - - - - Blur label ('intolerant') on quoted post (warn) @@ -540,11 +425,9 @@ Key: - - ✋ @@ -552,9 +435,6 @@ Key: - - - Blur label ('intolerant') on quoted author account (warn) @@ -562,11 +442,9 @@ Key: - - ✋ @@ -574,9 +452,6 @@ Key: - - - Blur label ('intolerant') on post (ignore) @@ -584,21 +459,15 @@ Key: - - - - - - Blur label ('intolerant') on author profile (ignore) @@ -606,21 +475,15 @@ Key: - - - - - - Blur label ('intolerant') on author account (ignore) @@ -628,21 +491,15 @@ Key: - - - - - - Blur label ('intolerant') on quoted post (ignore) @@ -650,21 +507,15 @@ Key: - - - - - - Blur label ('intolerant') on quoted author account (ignore) @@ -672,20 +523,15 @@ Key: - - - - - ScenarioFilterContentAvatarEmbed Blur-media label ('porn') on post (hide) @@ -694,11 +540,9 @@ Key: - - ✋ @@ -706,9 +550,6 @@ Key: - - - Blur-media label ('porn') on author profile (hide) @@ -716,7 +557,6 @@ Key: - ✋ @@ -724,13 +564,9 @@ Key: - - - - Blur-media label ('porn') on author account (hide) @@ -738,7 +574,6 @@ Key: - ✋ @@ -750,9 +585,6 @@ Key: - - - Blur-media label ('porn') on quoted post (hide) @@ -760,11 +592,9 @@ Key: - - ✋ @@ -772,9 +602,6 @@ Key: - - - Blur-media label ('porn') on quoted author account (hide) @@ -782,21 +609,15 @@ Key: - - - - - - Blur-media label ('porn') on post (warn) @@ -804,11 +625,9 @@ Key: - - ✋ @@ -816,9 +635,6 @@ Key: - - - Blur-media label ('porn') on author profile (warn) @@ -826,7 +642,6 @@ Key: - ✋ @@ -834,13 +649,9 @@ Key: - - - - Blur-media label ('porn') on author account (warn) @@ -848,7 +659,6 @@ Key: - ✋ @@ -860,9 +670,6 @@ Key: - - - Blur-media label ('porn') on quoted post (warn) @@ -870,11 +677,9 @@ Key: - - ✋ @@ -882,9 +687,6 @@ Key: - - - Blur-media label ('porn') on quoted author account (warn) @@ -892,21 +694,15 @@ Key: - - - - - - Blur-media label ('porn') on post (ignore) @@ -914,21 +710,15 @@ Key: - - - - - - Blur-media label ('porn') on author profile (ignore) @@ -936,21 +726,15 @@ Key: - - - - - - Blur-media label ('porn') on author account (ignore) @@ -958,21 +742,15 @@ Key: - - - - - - Blur-media label ('porn') on quoted post (ignore) @@ -980,21 +758,15 @@ Key: - - - - - - Blur-media label ('porn') on quoted author account (ignore) @@ -1002,20 +774,15 @@ Key: - - - - - ScenarioFilterContentAvatarEmbed Notice label ('scam') on post (hide) @@ -1025,20 +792,16 @@ Key: 🪧 + - - - - - Notice label ('scam') on author profile (hide) @@ -1046,21 +809,17 @@ Key: - 🪧 + - - - - Notice label ('scam') on author account (hide) @@ -1069,20 +828,18 @@ Key: 🪧 + 🪧 + - - - - Notice label ('scam') on quoted post (hide) @@ -1090,21 +847,17 @@ Key: - - 🪧 + - - - Notice label ('scam') on quoted author account (hide) @@ -1112,21 +865,17 @@ Key: - - 🪧 + - - - Notice label ('scam') on post (warn) @@ -1135,20 +884,16 @@ Key: 🪧 + - - - - - Notice label ('scam') on author profile (warn) @@ -1156,21 +901,17 @@ Key: - 🪧 + - - - - Notice label ('scam') on author account (warn) @@ -1179,20 +920,18 @@ Key: 🪧 + 🪧 + - - - - Notice label ('scam') on quoted post (warn) @@ -1200,21 +939,17 @@ Key: - - 🪧 + - - - Notice label ('scam') on quoted author account (warn) @@ -1222,21 +957,17 @@ Key: - - 🪧 + - - - Notice label ('scam') on post (ignore) @@ -1244,21 +975,15 @@ Key: - - - - - - Notice label ('scam') on author profile (ignore) @@ -1266,21 +991,15 @@ Key: - - - - - - Notice label ('scam') on author account (ignore) @@ -1288,21 +1007,15 @@ Key: - - - - - - Notice label ('scam') on quoted post (ignore) @@ -1310,21 +1023,15 @@ Key: - - - - - - Notice label ('scam') on quoted author account (ignore) @@ -1332,20 +1039,15 @@ Key: - - - - - ScenarioFilterContentAvatarEmbed Adult-only label on post when adult content is disabled @@ -1354,11 +1056,9 @@ Key: - - 🚫 @@ -1366,9 +1066,6 @@ Key: - - - Adult-only label on author profile when adult content is disabled @@ -1376,7 +1073,6 @@ Key: - 🚫 @@ -1384,13 +1080,9 @@ Key: - - - - Adult-only label on author account when adult content is disabled @@ -1398,7 +1090,6 @@ Key: - 🚫 @@ -1410,9 +1101,6 @@ Key: - - - Adult-only label on quoted post when adult content is disabled @@ -1420,11 +1108,9 @@ Key: - - 🚫 @@ -1432,9 +1118,6 @@ Key: - - - Adult-only label on quoted author account when adult content is disabled @@ -1442,20 +1125,15 @@ Key: - - - - - ScenarioFilterContentAvatarEmbed Self-post: Imperative label ('!hide') on post @@ -1468,17 +1146,12 @@ Key: - - - - - Self-post: Imperative label ('!hide') on author profile @@ -1486,21 +1159,15 @@ Key: - - - - - - Self-post: Imperative label ('!hide') on author account @@ -1508,21 +1175,15 @@ Key: - - - - - - Self-post: Imperative label ('!hide') on quoted post @@ -1530,11 +1191,9 @@ Key: - - ✋ @@ -1542,9 +1201,6 @@ Key: - - - Self-post: Imperative label ('!hide') on quoted author account @@ -1552,21 +1208,15 @@ Key: - - - - - - Self-post: Imperative label ('!warn') on post @@ -1578,17 +1228,12 @@ Key: - - - - - Self-post: Imperative label ('!warn') on author profile @@ -1596,21 +1241,15 @@ Key: - - - - - - Self-post: Imperative label ('!warn') on author account @@ -1618,21 +1257,15 @@ Key: - - - - - - Self-post: Imperative label ('!warn') on quoted post @@ -1640,11 +1273,9 @@ Key: - - ✋ @@ -1652,9 +1283,6 @@ Key: - - - Self-post: Imperative label ('!warn') on quoted author account @@ -1662,21 +1290,15 @@ Key: - - - - - - Self-post: Blur-media label ('porn') on post (hide) @@ -1684,11 +1306,9 @@ Key: - - ✋ @@ -1696,9 +1316,6 @@ Key: - - - Self-post: Blur-media label ('porn') on author profile (hide) @@ -1706,21 +1323,15 @@ Key: - - - - - - Self-post: Blur-media label ('porn') on author account (hide) @@ -1728,21 +1339,15 @@ Key: - - - - - - Self-post: Blur-media label ('porn') on quoted post (hide) @@ -1750,11 +1355,9 @@ Key: - - ✋ @@ -1762,9 +1365,6 @@ Key: - - - Self-post: Blur-media label ('porn') on quoted author account (hide) @@ -1772,21 +1372,15 @@ Key: - - - - - - Self-post: Blur-media label ('porn') on post (warn) @@ -1794,11 +1388,9 @@ Key: - - ✋ @@ -1806,9 +1398,6 @@ Key: - - - Self-post: Blur-media label ('porn') on author profile (warn) @@ -1816,21 +1405,15 @@ Key: - - - - - - Self-post: Blur-media label ('porn') on author account (warn) @@ -1838,21 +1421,15 @@ Key: - - - - - - Self-post: Blur-media label ('porn') on quoted post (warn) @@ -1860,11 +1437,9 @@ Key: - - ✋ @@ -1872,9 +1447,6 @@ Key: - - - Self-post: Blur-media label ('porn') on quoted author account (warn) @@ -1882,20 +1454,15 @@ Key: - - - - - ScenarioFilterContentAvatarEmbed Post with blocked author @@ -1912,13 +1479,9 @@ Key: - - - - Post with blocked quoted author @@ -1926,11 +1489,9 @@ Key: - - 🚫 @@ -1938,9 +1499,6 @@ Key: - - - Post with author blocking user @@ -1956,13 +1514,9 @@ Key: - - - - Post with quoted author blocking user @@ -1970,11 +1524,9 @@ Key: - - 🚫 @@ -1982,9 +1534,6 @@ Key: - - - Post with muted author @@ -1996,17 +1545,12 @@ Key: - - - - - Post with muted quoted author @@ -2014,11 +1558,9 @@ Key: - - ✋ @@ -2026,9 +1568,6 @@ Key: - - - Post with muted-by-list author @@ -2040,17 +1579,12 @@ Key: - - - - - Post with muted-by-list quoted author @@ -2058,11 +1592,9 @@ Key: - - ✋ @@ -2070,8 +1602,6 @@ Key: - - ScenarioFilterContentAvatarEmbed Prioritization: post with blocking & blocked-by author @@ -2088,13 +1618,9 @@ Key: - - - - Prioritization: post with blocking & blocked-by quoted author @@ -2102,11 +1628,9 @@ Key: - - 🚫 @@ -2114,9 +1638,6 @@ Key: - - - Prioritization: '!hide' label on post by blocked user @@ -2132,13 +1653,9 @@ Key: - - - - Prioritization: '!hide' label on quoted post, post by blocked user @@ -2158,9 +1675,6 @@ Key: - - - Prioritization: '!hide' and 'intolerant' labels on post (hide) @@ -2172,17 +1686,12 @@ Key: - - - - - Prioritization: '!warn' and 'intolerant' labels on post (hide) @@ -2194,17 +1703,12 @@ Key: - - - - - Prioritization: '!hide' and 'porn' labels on post (hide) @@ -2216,17 +1720,12 @@ Key: - - - - - Prioritization: '!warn' and 'porn' labels on post (hide) @@ -2234,11 +1733,9 @@ Key: - - ✋ @@ -2246,4 +1743,4 @@ Key: - \ No newline at end of file + diff --git a/packages/api/docs/moderation-behaviors/profiles.md b/packages/api/docs/moderation-behaviors/profiles.md index 3d2f9af96b3..b8d7c94ce91 100644 --- a/packages/api/docs/moderation-behaviors/profiles.md +++ b/packages/api/docs/moderation-behaviors/profiles.md @@ -38,7 +38,6 @@ Key: - 🚫 @@ -46,9 +45,6 @@ Key: - - - Imperative label ('!hide') on profile @@ -56,7 +52,6 @@ Key: - 🚫 @@ -68,9 +63,6 @@ Key: - - - Imperative label ('!no-promote') on account @@ -78,21 +70,15 @@ Key: - - - - - - Imperative label ('!no-promote') on profile @@ -100,21 +86,15 @@ Key: - - - - - - Imperative label ('!warn') on account @@ -126,7 +106,6 @@ Key: - ✋ @@ -134,9 +113,6 @@ Key: - - - Imperative label ('!warn') on profile @@ -144,7 +120,6 @@ Key: - ✋ @@ -156,8 +131,6 @@ Key: - - ScenarioFilterAccountProfileAvatar Blur label ('intolerant') on account (hide) @@ -170,7 +143,6 @@ Key: - ✋ @@ -178,9 +150,6 @@ Key: - - - Blur label ('intolerant') on profile (hide) @@ -188,7 +157,6 @@ Key: - ✋ @@ -200,9 +168,6 @@ Key: - - - Blur label ('intolerant') on account (warn) @@ -214,7 +179,6 @@ Key: - ✋ @@ -222,9 +186,6 @@ Key: - - - Blur label ('intolerant') on profile (warn) @@ -232,7 +193,6 @@ Key: - ✋ @@ -244,9 +204,6 @@ Key: - - - Blur label ('intolerant') on account (ignore) @@ -254,21 +211,15 @@ Key: - - - - - - Blur label ('intolerant') on profile (ignore) @@ -276,20 +227,15 @@ Key: - - - - - ScenarioFilterAccountProfileAvatar Blur-media label ('porn') on account (hide) @@ -302,7 +248,6 @@ Key: - ✋ @@ -310,9 +255,6 @@ Key: - - - Blur-media label ('porn') on profile (hide) @@ -320,11 +262,9 @@ Key: - - ✋ @@ -332,9 +272,6 @@ Key: - - - Blur-media label ('porn') on account (warn) @@ -346,7 +283,6 @@ Key: - ✋ @@ -354,9 +290,6 @@ Key: - - - Blur-media label ('porn') on profile (warn) @@ -364,11 +297,9 @@ Key: - - ✋ @@ -376,9 +307,6 @@ Key: - - - Blur-media label ('porn') on account (ignore) @@ -386,21 +314,15 @@ Key: - - - - - - Blur-media label ('porn') on profile (ignore) @@ -408,20 +330,15 @@ Key: - - - - - ScenarioFilterAccountProfileAvatar Notice label ('scam') on account (hide) @@ -431,20 +348,18 @@ Key: 🪧 + - 🪧 + - - - Notice label ('scam') on profile (hide) @@ -452,21 +367,19 @@ Key: - 🪧 + 🪧 + - - - Notice label ('scam') on account (warn) @@ -475,20 +388,18 @@ Key: 🪧 + - 🪧 + - - - Notice label ('scam') on profile (warn) @@ -496,21 +407,19 @@ Key: - 🪧 + 🪧 + - - - Notice label ('scam') on account (ignore) @@ -518,21 +427,15 @@ Key: - - - - - - Notice label ('scam') on profile (ignore) @@ -540,20 +443,15 @@ Key: - - - - - ScenarioFilterAccountProfileAvatar Adult-only label on account when adult content is disabled @@ -566,7 +464,6 @@ Key: - 🚫 @@ -574,9 +471,6 @@ Key: - - - Adult-only label on profile when adult content is disabled @@ -584,11 +478,9 @@ Key: - - 🚫 @@ -596,8 +488,6 @@ Key: - - ScenarioFilterAccountProfileAvatar Self-profile: !hide on account @@ -607,20 +497,18 @@ Key: 🪧 + - 🪧 + - - - Self-profile: !hide on profile @@ -628,20 +516,19 @@ Key: - 🪧 + 🪧 + - - ScenarioFilterAccountProfileAvatar Mute/block: Blocking user @@ -650,11 +537,9 @@ Key: - - 🚫 @@ -662,9 +547,6 @@ Key: - - - Mute/block: Blocked by user @@ -672,11 +554,9 @@ Key: - - 🚫 @@ -684,9 +564,6 @@ Key: - - - Mute/block: Muted user @@ -694,21 +571,15 @@ Key: - - - - - - Mute/block: Muted-by-list user @@ -716,20 +587,15 @@ Key: - - - - - ScenarioFilterAccountProfileAvatar Prioritization: blocking & blocked-by user @@ -738,11 +604,9 @@ Key: - - 🚫 @@ -750,9 +614,6 @@ Key: - - - Prioritization: '!hide' label on account of blocked user @@ -764,7 +625,6 @@ Key: - 🚫 @@ -772,9 +632,6 @@ Key: - - - Prioritization: '!hide' and 'intolerant' labels on account (hide) @@ -786,7 +643,6 @@ Key: - 🚫 @@ -794,9 +650,6 @@ Key: - - - Prioritization: '!warn' and 'intolerant' labels on account (hide) @@ -808,7 +661,6 @@ Key: - ✋ @@ -816,9 +668,6 @@ Key: - - - Prioritization: '!warn' and 'porn' labels on account (hide) @@ -830,7 +679,6 @@ Key: - ✋ @@ -838,9 +686,6 @@ Key: - - - Prioritization: intolerant label on account (hide) and scam label on profile (warn) @@ -853,6 +698,7 @@ Key: 🪧 + ✋ @@ -860,9 +706,6 @@ Key: - - - Prioritization: !hide on account, !warn on profile @@ -882,9 +725,6 @@ Key: - - - Prioritization: !warn on account, !hide on profile @@ -904,4 +744,4 @@ Key: - \ No newline at end of file + diff --git a/packages/api/docs/moderation.md b/packages/api/docs/moderation.md index b3d1ecc9547..23446f11b54 100644 --- a/packages/api/docs/moderation.md +++ b/packages/api/docs/moderation.md @@ -80,7 +80,7 @@ interface LabelerSettings { Applications need to produce the [Post Moderation Behaviors](./moderation-behaviors/posts.md) using the `moderatePost()` API. ```typescript -import {moderatePost} from '@atproto/api' +import { moderatePost } from '@atproto/api' const postMod = moderatePost(postView, getOpts()) @@ -119,7 +119,7 @@ if (postMod.avatar.alert) { Applications need to produce the [Profile Moderation Behaviors](./moderation-behaviors/profiles.md) using the `moderateProfile()` API. ```typescript -import {moderateProfile} from '@atproto/api' +import { moderateProfile } from '@atproto/api' const profileMod = moderateProfile(profileView, getOpts()) @@ -150,4 +150,4 @@ if (profileMod.avatar.blur) { if (profileMod.avatar.alert) { // render an alert on the avatar } -``` \ No newline at end of file +``` diff --git a/packages/api/tsconfig.build.json b/packages/api/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/api/tsconfig.build.json +++ b/packages/api/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/aws/README.md b/packages/aws/README.md index 19e36786da6..9a32e52b36d 100644 --- a/packages/aws/README.md +++ b/packages/aws/README.md @@ -1,3 +1,3 @@ # AWS KMS -A Keypair-compatible wrapper for AWS KMS. \ No newline at end of file +A Keypair-compatible wrapper for AWS KMS. diff --git a/packages/aws/package.json b/packages/aws/package.json index 192149feb69..70fe9f72698 100644 --- a/packages/aws/package.json +++ b/packages/aws/package.json @@ -13,12 +13,6 @@ "directory": "packages/aws" }, "scripts": { - "prettier": "prettier --check src/", - "prettier:fix": "prettier --write src/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/aws" diff --git a/packages/aws/tsconfig.build.json b/packages/aws/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/aws/tsconfig.build.json +++ b/packages/aws/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/aws/tsconfig.json b/packages/aws/tsconfig.json index 624cc32a35f..fee83b7f23b 100644 --- a/packages/aws/tsconfig.json +++ b/packages/aws/tsconfig.json @@ -4,5 +4,5 @@ "outDir": "./dist", // Your outDir, "emitDeclarationOnly": true }, - "include": ["./src","__tests__/**/**.ts"] -} \ No newline at end of file + "include": ["./src", "__tests__/**/**.ts"] +} diff --git a/packages/bsky/.eslintignore b/packages/bsky/.eslintignore deleted file mode 100644 index 18dd0f354fc..00000000000 --- a/packages/bsky/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -lexicon \ No newline at end of file diff --git a/packages/bsky/.prettierignore b/packages/bsky/.prettierignore deleted file mode 100644 index dd7966f095f..00000000000 --- a/packages/bsky/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -src/lexicon/**/* diff --git a/packages/bsky/package.json b/packages/bsky/package.json index 177435387cd..508d9ef569f 100644 --- a/packages/bsky/package.json +++ b/packages/bsky/package.json @@ -22,12 +22,6 @@ "test": "../dev-infra/with-test-redis-and-db.sh jest", "test:log": "tail -50 test.log | pino-pretty", "test:updateSnapshot": "jest --updateSnapshot", - "prettier": "prettier --check src/ tests/", - "prettier:fix": "prettier --write src/ tests/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "migration:create": "ts-node ./bin/migration-create.ts" }, "dependencies": { diff --git a/packages/common-web/package.json b/packages/common-web/package.json index fb1552e0133..92387c502ab 100644 --- a/packages/common-web/package.json +++ b/packages/common-web/package.json @@ -14,12 +14,6 @@ }, "scripts": { "test": "jest", - "prettier": "prettier --check src/ tests/", - "prettier:fix": "prettier --write src/ tests/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/common-web" diff --git a/packages/common-web/tsconfig.build.json b/packages/common-web/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/common-web/tsconfig.build.json +++ b/packages/common-web/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/common-web/tsconfig.json b/packages/common-web/tsconfig.json index c50c08b2e80..5c5ec40ce03 100644 --- a/packages/common-web/tsconfig.json +++ b/packages/common-web/tsconfig.json @@ -5,5 +5,5 @@ "outDir": "./dist", // Your outDir, "emitDeclarationOnly": true }, - "include": ["./src","__tests__/**/**.ts"], -} \ No newline at end of file + "include": ["./src", "__tests__/**/**.ts"] +} diff --git a/packages/common/package.json b/packages/common/package.json index d87d7190287..c7f518c1a99 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -14,12 +14,6 @@ }, "scripts": { "test": "jest", - "prettier": "prettier --check src/ tests/", - "prettier:fix": "prettier --write src/ tests/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/common" diff --git a/packages/common/tsconfig.build.json b/packages/common/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/common/tsconfig.build.json +++ b/packages/common/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/common/tsconfig.json b/packages/common/tsconfig.json index c837279cd68..81714d5ec97 100644 --- a/packages/common/tsconfig.json +++ b/packages/common/tsconfig.json @@ -5,8 +5,6 @@ "outDir": "./dist", // Your outDir, "emitDeclarationOnly": true }, - "include": ["./src","__tests__/**/**.ts"], - "references": [ - { "path": "../common-web/tsconfig.build.json" } - ] -} \ No newline at end of file + "include": ["./src", "__tests__/**/**.ts"], + "references": [{ "path": "../common-web/tsconfig.build.json" }] +} diff --git a/packages/crypto/README.md b/packages/crypto/README.md index 062eb95ec2b..2258451f7ca 100644 --- a/packages/crypto/README.md +++ b/packages/crypto/README.md @@ -1,3 +1,3 @@ # ATP Crypto Library -ATP's common cryptographic operations. \ No newline at end of file +ATP's common cryptographic operations. diff --git a/packages/crypto/jest.config.js b/packages/crypto/jest.config.js index 8b8fc38538f..365131c9293 100644 --- a/packages/crypto/jest.config.js +++ b/packages/crypto/jest.config.js @@ -2,5 +2,5 @@ const base = require('../../jest.config.base.js') module.exports = { ...base, - displayName: 'Crypto' + displayName: 'Crypto', } diff --git a/packages/crypto/package.json b/packages/crypto/package.json index 8bc10ae7f2d..c1d7fa5b521 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -14,12 +14,6 @@ }, "scripts": { "test": "jest ", - "prettier": "prettier --check src/ tests/", - "prettier:fix": "prettier --write src/ tests/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/crypto" diff --git a/packages/crypto/tsconfig.build.json b/packages/crypto/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/crypto/tsconfig.build.json +++ b/packages/crypto/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/crypto/tsconfig.json b/packages/crypto/tsconfig.json index afddb2bd4e1..5c5ec40ce03 100644 --- a/packages/crypto/tsconfig.json +++ b/packages/crypto/tsconfig.json @@ -5,5 +5,5 @@ "outDir": "./dist", // Your outDir, "emitDeclarationOnly": true }, - "include": ["./src","__tests__/**/**.ts"] -} \ No newline at end of file + "include": ["./src", "__tests__/**/**.ts"] +} diff --git a/packages/dev-env/README.md b/packages/dev-env/README.md index 442f7110eb1..c1a41e692fe 100644 --- a/packages/dev-env/README.md +++ b/packages/dev-env/README.md @@ -24,4 +24,4 @@ Create a new user. ### `user(handle: string): ServiceClient` -Get the `ServiceClient` for the given user. \ No newline at end of file +Get the `ServiceClient` for the given user. diff --git a/packages/dev-env/package.json b/packages/dev-env/package.json index a1f9291d1f7..adc1a4e21c8 100644 --- a/packages/dev-env/package.json +++ b/packages/dev-env/package.json @@ -18,13 +18,7 @@ "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/dev-env", "start": "node dist/bin.js", - "start:network": "../dev-infra/with-test-redis-and-db.sh node dist/bin-network.js", - "prettier": "prettier --check src/", - "prettier:fix": "prettier --write src/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix" + "start:network": "../dev-infra/with-test-redis-and-db.sh node dist/bin-network.js" }, "dependencies": { "@atproto/api": "workspace:^", diff --git a/packages/dev-env/tsconfig.build.json b/packages/dev-env/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/dev-env/tsconfig.build.json +++ b/packages/dev-env/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/dev-env/tsconfig.json b/packages/dev-env/tsconfig.json index 57cab99ed25..6e341637305 100644 --- a/packages/dev-env/tsconfig.json +++ b/packages/dev-env/tsconfig.json @@ -6,13 +6,13 @@ "emitDeclarationOnly": true }, "module": "esnext", - "include": ["./src","__tests__/**/**.ts"], + "include": ["./src", "__tests__/**/**.ts"], "references": [ { "path": "../api/tsconfig.build.json" }, { "path": "../bsky/tsconfig.build.json" }, { "path": "../crypto/tsconfig.build.json" }, { "path": "../identity/tsconfig.build.json" }, { "path": "../pds/tsconfig.json" }, - { "path": "../uri/tsconfig.build.json" }, + { "path": "../uri/tsconfig.build.json" } ] -} \ No newline at end of file +} diff --git a/packages/identifier/package.json b/packages/identifier/package.json index c36e0feced7..1642c1046d0 100644 --- a/packages/identifier/package.json +++ b/packages/identifier/package.json @@ -8,12 +8,6 @@ }, "scripts": { "test": "true", - "prettier": "prettier --check src/", - "prettier:fix": "prettier --write src/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/identifier" diff --git a/packages/identifier/tsconfig.build.json b/packages/identifier/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/identifier/tsconfig.build.json +++ b/packages/identifier/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/identifier/tsconfig.json b/packages/identifier/tsconfig.json index aaa3b2d88bf..db7a7c4ad35 100644 --- a/packages/identifier/tsconfig.json +++ b/packages/identifier/tsconfig.json @@ -5,8 +5,6 @@ "outDir": "./dist", // Your outDir, "emitDeclarationOnly": true }, - "include": ["./src","__tests__/**/**.ts"], - "references": [ - { "path": "../common/tsconfig.build.json" }, - ] -} \ No newline at end of file + "include": ["./src", "__tests__/**/**.ts"], + "references": [{ "path": "../common/tsconfig.build.json" }] +} diff --git a/packages/identity/README.md b/packages/identity/README.md index 0a252d6ede2..c6d940f448e 100644 --- a/packages/identity/README.md +++ b/packages/identity/README.md @@ -1,3 +1,3 @@ # ATP DID Resolver -A library for resolving ATP's Decentralized ID methods. \ No newline at end of file +A library for resolving ATP's Decentralized ID methods. diff --git a/packages/identity/jest.config.js b/packages/identity/jest.config.js index 85c2bc4cf32..4eb023d6fcc 100644 --- a/packages/identity/jest.config.js +++ b/packages/identity/jest.config.js @@ -2,5 +2,5 @@ const base = require('../../jest.config.base.js') module.exports = { ...base, - displayName: 'Identity' + displayName: 'Identity', } diff --git a/packages/identity/package.json b/packages/identity/package.json index 5399698b1c3..1bd0e3cc3f4 100644 --- a/packages/identity/package.json +++ b/packages/identity/package.json @@ -15,12 +15,6 @@ "scripts": { "test": "jest", "test:log": "cat test.log | pino-pretty", - "prettier": "prettier --check src/ tests/", - "prettier:fix": "prettier --write src/ tests/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/identity" diff --git a/packages/identity/tsconfig.build.json b/packages/identity/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/identity/tsconfig.build.json +++ b/packages/identity/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/identity/tsconfig.json b/packages/identity/tsconfig.json index bcc1381b4ff..9d6fd378666 100644 --- a/packages/identity/tsconfig.json +++ b/packages/identity/tsconfig.json @@ -5,9 +5,9 @@ "outDir": "./dist", // Your outDir, "emitDeclarationOnly": true }, - "include": ["./src","__tests__/**/**.ts"], + "include": ["./src", "__tests__/**/**.ts"], "references": [ { "path": "../common/tsconfig.build.json" }, - { "path": "../crypto/tsconfig.build.json" }, + { "path": "../crypto/tsconfig.build.json" } ] -} \ No newline at end of file +} diff --git a/packages/lex-cli/README.md b/packages/lex-cli/README.md index d898002e092..1eee8f0c5ac 100644 --- a/packages/lex-cli/README.md +++ b/packages/lex-cli/README.md @@ -36,4 +36,4 @@ $ lex gen-server ./server/src/xrpc ./schemas/com/service/*.json ./schemas/com/an ## License -MIT \ No newline at end of file +MIT diff --git a/packages/lex-cli/package.json b/packages/lex-cli/package.json index 23b85686031..6a79be7d34c 100644 --- a/packages/lex-cli/package.json +++ b/packages/lex-cli/package.json @@ -10,12 +10,6 @@ "types": "dist/src/index.d.ts" }, "scripts": { - "prettier": "prettier --check src/", - "prettier:fix": "prettier --write src/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/lex-cli" diff --git a/packages/lex-cli/tsconfig.build.json b/packages/lex-cli/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/lex-cli/tsconfig.build.json +++ b/packages/lex-cli/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/lex-cli/tsconfig.json b/packages/lex-cli/tsconfig.json index 41e06d6657d..bdf4c2a099a 100644 --- a/packages/lex-cli/tsconfig.json +++ b/packages/lex-cli/tsconfig.json @@ -4,9 +4,9 @@ "outDir": "./dist", // Your outDir, "emitDeclarationOnly": true }, - "include": ["./src","__tests__/**/**.ts"], + "include": ["./src", "__tests__/**/**.ts"], "references": [ { "path": "../nsid/tsconfig.build.json" }, - { "path": "../lexicon/tsconfig.build.json" }, + { "path": "../lexicon/tsconfig.build.json" } ] -} \ No newline at end of file +} diff --git a/packages/lexicon/package.json b/packages/lexicon/package.json index cfd10bd7246..4b5f7470671 100644 --- a/packages/lexicon/package.json +++ b/packages/lexicon/package.json @@ -8,12 +8,6 @@ }, "scripts": { "test": "jest", - "prettier": "prettier --check src/ tests/", - "prettier:fix": "prettier --write src/ tests/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/lexicon" diff --git a/packages/lexicon/tsconfig.build.json b/packages/lexicon/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/lexicon/tsconfig.build.json +++ b/packages/lexicon/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/lexicon/tsconfig.json b/packages/lexicon/tsconfig.json index 4b0566e70fc..5e60cf9920e 100644 --- a/packages/lexicon/tsconfig.json +++ b/packages/lexicon/tsconfig.json @@ -11,4 +11,4 @@ { "path": "../nsid/tsconfig.build.json" }, { "path": "../uri/tsconfig.build.json" } ] -} \ No newline at end of file +} diff --git a/packages/nsid/README.md b/packages/nsid/README.md index a3455b6dbc3..180a88338bb 100644 --- a/packages/nsid/README.md +++ b/packages/nsid/README.md @@ -6,18 +6,18 @@ import { NSID } from '@atproto/nsid' const id1 = NSID.parse('com.example.foo') -id1.authority // => 'example.com' -id1.name // => 'foo' +id1.authority // => 'example.com' +id1.name // => 'foo' id1.toString() // => 'com.example.foo' const id2 = NSID.create('example.com', 'foo') -id2.authority // => 'example.com' -id2.name // => 'foo' +id2.authority // => 'example.com' +id2.name // => 'foo' id2.toString() // => 'com.example.foo' const id3 = NSID.create('example.com', 'someRecord') -id3.authority // => 'example.com' -id3.name // => 'someRecord' +id3.authority // => 'example.com' +id3.name // => 'someRecord' id3.toString() // => 'com.example.someRecord' NSID.isValid('com.example.foo') // => true diff --git a/packages/nsid/package.json b/packages/nsid/package.json index 9f9e05c27f8..af156718f49 100644 --- a/packages/nsid/package.json +++ b/packages/nsid/package.json @@ -8,12 +8,6 @@ }, "scripts": { "test": "true", - "prettier": "prettier --check src/", - "prettier:fix": "prettier --write src/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/nsid" diff --git a/packages/nsid/tsconfig.build.json b/packages/nsid/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/nsid/tsconfig.build.json +++ b/packages/nsid/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/nsid/tsconfig.json b/packages/nsid/tsconfig.json index c1364fd69c1..fee83b7f23b 100644 --- a/packages/nsid/tsconfig.json +++ b/packages/nsid/tsconfig.json @@ -4,5 +4,5 @@ "outDir": "./dist", // Your outDir, "emitDeclarationOnly": true }, - "include": ["./src","__tests__/**/**.ts"], -} \ No newline at end of file + "include": ["./src", "__tests__/**/**.ts"] +} diff --git a/packages/pds/.eslintignore b/packages/pds/.eslintignore deleted file mode 100644 index 18dd0f354fc..00000000000 --- a/packages/pds/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -lexicon \ No newline at end of file diff --git a/packages/pds/.prettierignore b/packages/pds/.prettierignore deleted file mode 100644 index e74f52e0ce8..00000000000 --- a/packages/pds/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -src/lexicon/**/* -src/mailer/templates/**/* \ No newline at end of file diff --git a/packages/pds/README.md b/packages/pds/README.md index 81f32ad0791..547856de3d0 100644 --- a/packages/pds/README.md +++ b/packages/pds/README.md @@ -1,3 +1,3 @@ # ATP Personal Data Server (PDS) -The Personal Data Server (PDS). This is ATP's main server-side implementation. \ No newline at end of file +The Personal Data Server (PDS). This is ATP's main server-side implementation. diff --git a/packages/pds/package.json b/packages/pds/package.json index e99b80be0b6..488088942d6 100644 --- a/packages/pds/package.json +++ b/packages/pds/package.json @@ -24,12 +24,6 @@ "test:sqlite": "jest --testPathIgnorePatterns /tests/proxied/*", "test:log": "tail -50 test.log | pino-pretty", "test:updateSnapshot": "jest --updateSnapshot", - "prettier": "prettier --check src/ tests/", - "prettier:fix": "prettier --write src/ tests/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "migration:create": "ts-node ./bin/migration-create.ts" }, "dependencies": { diff --git a/packages/pds/src/mailer/templates/delete-account.hbs b/packages/pds/src/mailer/templates/delete-account.hbs index 67f444dcd6d..f7fbf07a539 100644 --- a/packages/pds/src/mailer/templates/delete-account.hbs +++ b/packages/pds/src/mailer/templates/delete-account.hbs @@ -169,7 +169,10 @@ class="text-red-700" style="color: #b91c1c; padding-top: 0; padding-bottom: 0; font-weight: 500; vertical-align: baseline; font-size: 20px; line-height: 24px; margin: 0;" align="left" - >This will permanently delete your Bluesky account. If you did not issue this request, please update your password. + >This will permanently delete your + Bluesky account. If you did not + issue this request, please update + your password. To permanently delete your account, please - enter the above "reset code" in - the app along with your password.

+ >To + permanently delete + your account, please enter the + above "reset code" in the app + along with your password.

diff --git a/packages/pds/tsconfig.build.json b/packages/pds/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/pds/tsconfig.build.json +++ b/packages/pds/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/pds/tsconfig.json b/packages/pds/tsconfig.json index c2b548249fe..9505b44db69 100644 --- a/packages/pds/tsconfig.json +++ b/packages/pds/tsconfig.json @@ -3,10 +3,10 @@ "compilerOptions": { "rootDir": "./src", "outDir": "./dist", // Your outDir, - "emitDeclarationOnly": true, + "emitDeclarationOnly": true }, "module": "nodenext", - "include": ["./src","__tests__/**/**.ts"], + "include": ["./src", "__tests__/**/**.ts"], "references": [ { "path": "../api/tsconfig.build.json" }, { "path": "../common/tsconfig.build.json" }, @@ -19,4 +19,4 @@ { "path": "../uri/tsconfig.build.json" }, { "path": "../xrpc-server/tsconfig.build.json" } ] -} \ No newline at end of file +} diff --git a/packages/repo/README.md b/packages/repo/README.md index f17f7c0235f..53c83a3071c 100644 --- a/packages/repo/README.md +++ b/packages/repo/README.md @@ -1,3 +1,3 @@ # ATP Repo -The "ATP repository" core implementation (a Merkle Search Tree). \ No newline at end of file +The "ATP repository" core implementation (a Merkle Search Tree). diff --git a/packages/repo/package.json b/packages/repo/package.json index 96dc165f56f..9c37695f1d8 100644 --- a/packages/repo/package.json +++ b/packages/repo/package.json @@ -17,12 +17,6 @@ "test:profile": "node --inspect ../../node_modules/.bin/jest", "bench": "jest --config jest.bench.config.js", "bench:profile": "node --inspect-brk ../../node_modules/.bin/jest --config jest.bench.config.js", - "prettier": "prettier --check src/ tests/", - "prettier:fix": "prettier --write src/ tests/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/repo" diff --git a/packages/repo/tsconfig.build.json b/packages/repo/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/repo/tsconfig.build.json +++ b/packages/repo/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/repo/tsconfig.json b/packages/repo/tsconfig.json index 855d51d8af6..07c54d2c4d7 100644 --- a/packages/repo/tsconfig.json +++ b/packages/repo/tsconfig.json @@ -5,11 +5,11 @@ "outDir": "./dist", // Your outDir, "emitDeclarationOnly": true }, - "include": ["./src","__tests__/**/**.ts"], + "include": ["./src", "__tests__/**/**.ts"], "references": [ { "path": "../common/tsconfig.build.json" }, { "path": "../crypto/tsconfig.build.json" }, { "path": "../identity/tsconfig.build.json" }, - { "path": "../nsid/tsconfig.build.json" }, + { "path": "../nsid/tsconfig.build.json" } ] -} \ No newline at end of file +} diff --git a/packages/syntax/README.md b/packages/syntax/README.md index e9c5d151609..5fd557fa323 100644 --- a/packages/syntax/README.md +++ b/packages/syntax/README.md @@ -23,18 +23,18 @@ ensureValidDid(':did:method:val') // throws import { NSID } from '@atproto/syntax' const id1 = NSID.parse('com.example.foo') -id1.authority // => 'example.com' -id1.name // => 'foo' +id1.authority // => 'example.com' +id1.name // => 'foo' id1.toString() // => 'com.example.foo' const id2 = NSID.create('example.com', 'foo') -id2.authority // => 'example.com' -id2.name // => 'foo' +id2.authority // => 'example.com' +id2.name // => 'foo' id2.toString() // => 'com.example.foo' const id3 = NSID.create('example.com', 'someRecord') -id3.authority // => 'example.com' -id3.name // => 'someRecord' +id3.authority // => 'example.com' +id3.name // => 'someRecord' id3.toString() // => 'com.example.someRecord' NSID.isValid('com.example.foo') // => true @@ -49,11 +49,11 @@ NSID.isValid('foo') // => false import { AtUri } from '@atproto/syntax' const uri = new AtUri('at://bob.com/com.example.post/1234') -uri.protocol // => 'at:' -uri.origin // => 'at://bob.com' -uri.hostname // => 'bob.com' +uri.protocol // => 'at:' +uri.origin // => 'at://bob.com' +uri.hostname // => 'bob.com' uri.collection // => 'com.example.post' -uri.rkey // => '1234' +uri.rkey // => '1234' ``` ## License diff --git a/packages/syntax/package.json b/packages/syntax/package.json index 943123e5946..60fabdfe8d2 100644 --- a/packages/syntax/package.json +++ b/packages/syntax/package.json @@ -8,12 +8,6 @@ }, "scripts": { "test": "jest", - "prettier": "prettier --check src/ tests/", - "prettier:fix": "prettier --write src/ tests/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/syntax" diff --git a/packages/syntax/tsconfig.build.json b/packages/syntax/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/syntax/tsconfig.build.json +++ b/packages/syntax/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/syntax/tsconfig.json b/packages/syntax/tsconfig.json index aaa3b2d88bf..db7a7c4ad35 100644 --- a/packages/syntax/tsconfig.json +++ b/packages/syntax/tsconfig.json @@ -5,8 +5,6 @@ "outDir": "./dist", // Your outDir, "emitDeclarationOnly": true }, - "include": ["./src","__tests__/**/**.ts"], - "references": [ - { "path": "../common/tsconfig.build.json" }, - ] -} \ No newline at end of file + "include": ["./src", "__tests__/**/**.ts"], + "references": [{ "path": "../common/tsconfig.build.json" }] +} diff --git a/packages/uri/README.md b/packages/uri/README.md index 43c8bc9e435..8aaee47f7f2 100644 --- a/packages/uri/README.md +++ b/packages/uri/README.md @@ -6,13 +6,13 @@ import { AtUri } from '@atproto/uri' const uri = new AtUri('at://bob.com/com.example.post/1234') -uri.protocol // => 'at:' -uri.origin // => 'at://bob.com' -uri.hostname // => 'bob.com' +uri.protocol // => 'at:' +uri.origin // => 'at://bob.com' +uri.hostname // => 'bob.com' uri.collection // => 'com.example.post' -uri.rkey // => '1234' +uri.rkey // => '1234' ``` ## License -MIT \ No newline at end of file +MIT diff --git a/packages/uri/package.json b/packages/uri/package.json index 10b91e7f267..1060dda6890 100644 --- a/packages/uri/package.json +++ b/packages/uri/package.json @@ -8,12 +8,6 @@ }, "scripts": { "test": "true", - "prettier": "prettier --check src/", - "prettier:fix": "prettier --write src/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/uri" diff --git a/packages/uri/tsconfig.build.json b/packages/uri/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/uri/tsconfig.build.json +++ b/packages/uri/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/uri/tsconfig.json b/packages/uri/tsconfig.json index b678da34064..4faf3966f41 100644 --- a/packages/uri/tsconfig.json +++ b/packages/uri/tsconfig.json @@ -5,9 +5,9 @@ "outDir": "./dist", // Your outDir, "emitDeclarationOnly": true }, - "include": ["./src","__tests__/**/**.ts"], + "include": ["./src", "__tests__/**/**.ts"], "references": [ { "path": "../identifier/tsconfig.build.json" }, - { "path": "../nsid/tsconfig.build.json" }, + { "path": "../nsid/tsconfig.build.json" } ] -} \ No newline at end of file +} diff --git a/packages/xrpc-server/README.md b/packages/xrpc-server/README.md index 65e930e4e33..2c297043fdf 100644 --- a/packages/xrpc-server/README.md +++ b/packages/xrpc-server/README.md @@ -7,7 +7,8 @@ import * as xrpc from '@atproto/xrpc-server' import express from 'express' // create xrpc server -const server = xrpc.createServer([{ +const server = xrpc.createServer([ + { lexicon: 1, id: 'io.example.ping', defs: { @@ -22,11 +23,17 @@ const server = xrpc.createServer([{ }, }, }, - } + }, ]) -function ping(ctx: {auth: xrpc.HandlerAuth | undefined, params: xrpc.Params, input: xrpc.HandlerInput | undefined, req: express.Request, res: express.Response}) { - return { encoding: 'application/json', body: {message: ctx.params.message }} +function ping(ctx: { + auth: xrpc.HandlerAuth | undefined + params: xrpc.Params + input: xrpc.HandlerInput | undefined + req: express.Request + res: express.Response +}) { + return { encoding: 'application/json', body: { message: ctx.params.message } } } server.method('io.example.ping', ping) diff --git a/packages/xrpc-server/package.json b/packages/xrpc-server/package.json index 4901b53f638..e51db0f0a0f 100644 --- a/packages/xrpc-server/package.json +++ b/packages/xrpc-server/package.json @@ -8,12 +8,6 @@ }, "scripts": { "test": "jest", - "prettier": "prettier --check src/ tests/", - "prettier:fix": "prettier --write src/ tests/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/xrpc-server" diff --git a/packages/xrpc-server/tsconfig.build.json b/packages/xrpc-server/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/xrpc-server/tsconfig.build.json +++ b/packages/xrpc-server/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/xrpc-server/tsconfig.json b/packages/xrpc-server/tsconfig.json index 8ef03980175..6a9622be37f 100644 --- a/packages/xrpc-server/tsconfig.json +++ b/packages/xrpc-server/tsconfig.json @@ -5,11 +5,11 @@ "outDir": "./dist", // Your outDir, "emitDeclarationOnly": true }, - "include": ["./src","__tests__/**/**.ts"], + "include": ["./src", "__tests__/**/**.ts"], "references": [ { "path": "../common/tsconfig.build.json" }, { "path": "../crypto/tsconfig.build.json" }, { "path": "../lexicon/tsconfig.build.json" }, - { "path": "../xrpc/tsconfig.build.json" }, + { "path": "../xrpc/tsconfig.build.json" } ] -} \ No newline at end of file +} diff --git a/packages/xrpc/README.md b/packages/xrpc/README.md index 4b48e052cae..bfb0b8778c3 100644 --- a/packages/xrpc/README.md +++ b/packages/xrpc/README.md @@ -14,24 +14,28 @@ xrpc.addLexicon({ description: 'Ping the server', parameters: { type: 'params', - properties: {message: { type: 'string' }} + properties: { message: { type: 'string' } }, }, output: { encoding: 'application/json', schema: { type: 'object', required: ['message'], - properties: {message: { type: 'string' }}, + properties: { message: { type: 'string' } }, }, - } + }, }, }, }) -const res1 = await xrpc.call('https://example.com', 'io.example.ping', {message: 'hello world'}) +const res1 = await xrpc.call('https://example.com', 'io.example.ping', { + message: 'hello world', +}) res1.encoding // => 'application/json' res1.body // => {message: 'hello world'} -const res2 = await xrpc.service('https://example.com').call('io.example.ping', {message: 'hello world'}) +const res2 = await xrpc + .service('https://example.com') + .call('io.example.ping', { message: 'hello world' }) res2.encoding // => 'application/json' res2.body // => {message: 'hello world'} @@ -44,21 +48,20 @@ xrpc.addLexicon({ description: 'Write a JSON file', parameters: { type: 'params', - properties: {fileName: { type: 'string' }}, + properties: { fileName: { type: 'string' } }, }, input: { - encoding: 'application/json' + encoding: 'application/json', }, }, }, }) -const res3 = await xrpc - .service('https://example.com') - .call('io.example.writeJsonFile', - {fileName: 'foo.json'}, // query parameters - {hello: 'world', thisIs: 'the file to write'} // input body - ) +const res3 = await xrpc.service('https://example.com').call( + 'io.example.writeJsonFile', + { fileName: 'foo.json' }, // query parameters + { hello: 'world', thisIs: 'the file to write' }, // input body +) ``` ## License diff --git a/packages/xrpc/package.json b/packages/xrpc/package.json index 2949497b5ca..4fd825acd06 100644 --- a/packages/xrpc/package.json +++ b/packages/xrpc/package.json @@ -7,12 +7,6 @@ "types": "dist/index.d.ts" }, "scripts": { - "prettier": "prettier --check src/", - "prettier:fix": "prettier --write src/", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "pnpm lint --fix", - "verify": "run-p prettier lint", - "verify:fix": "pnpm prettier:fix && pnpm lint:fix", "build": "node ./build.js", "postbuild": "tsc --build tsconfig.build.json", "update-main-to-dist": "node ../../update-main-to-dist.js packages/xrpc" diff --git a/packages/xrpc/tsconfig.build.json b/packages/xrpc/tsconfig.build.json index 27df65b89e2..02a84823b65 100644 --- a/packages/xrpc/tsconfig.build.json +++ b/packages/xrpc/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "exclude": ["**/*.spec.ts", "**/*.test.ts"] -} \ No newline at end of file +} diff --git a/packages/xrpc/tsconfig.json b/packages/xrpc/tsconfig.json index 0b51fb2835b..72f1ae0e0f3 100644 --- a/packages/xrpc/tsconfig.json +++ b/packages/xrpc/tsconfig.json @@ -5,9 +5,9 @@ "outDir": "./dist", // Your outDir, "emitDeclarationOnly": true }, - "include": ["./src","__tests__/**/**.ts"], + "include": ["./src", "__tests__/**/**.ts"], "references": [ { "path": "../common/tsconfig.build.json" }, { "path": "../lexicon/tsconfig.build.json" } ] -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf41041ac50..1898a37f098 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,9 +71,6 @@ importers: node-gyp: specifier: ^9.3.1 version: 9.3.1 - npm-run-all: - specifier: ^4.1.5 - version: 4.1.5 pino-pretty: specifier: ^9.1.0 version: 9.1.0 @@ -6464,17 +6461,6 @@ packages: which: 1.3.1 dev: true - /cross-spawn@6.0.5: - resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} - engines: {node: '>=4.8'} - dependencies: - nice-try: 1.0.5 - path-key: 2.0.1 - semver: 5.7.2 - shebang-command: 1.2.0 - which: 1.3.1 - dev: true - /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -6816,7 +6802,7 @@ packages: call-bind: 1.0.2 es-set-tostringtag: 2.0.1 es-to-primitive: 1.2.1 - function.prototype.name: 1.1.5 + function.prototype.name: 1.1.6 get-intrinsic: 1.2.1 get-symbol-description: 1.0.0 globalthis: 1.0.3 @@ -6838,11 +6824,11 @@ packages: object-keys: 1.1.1 object.assign: 4.1.4 regexp.prototype.flags: 1.5.0 - safe-array-concat: 1.0.0 + safe-array-concat: 1.0.1 safe-regex-test: 1.0.0 string.prototype.trim: 1.2.7 string.prototype.trimend: 1.0.6 - string.prototype.trimstart: 1.0.6 + string.prototype.trimstart: 1.0.7 typed-array-buffer: 1.0.0 typed-array-byte-length: 1.0.0 typed-array-byte-offset: 1.0.0 @@ -7640,8 +7626,8 @@ packages: /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - /function.prototype.name@1.1.5: - resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} engines: {node: '>= 0.4'} dependencies: call-bind: 1.0.2 @@ -8786,10 +8772,6 @@ packages: hasBin: true dev: true - /json-parse-better-errors@1.0.2: - resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} - dev: true - /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} dev: true @@ -8924,16 +8906,6 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: true - /load-json-file@4.0.0: - resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} - engines: {node: '>=4'} - dependencies: - graceful-fs: 4.2.11 - parse-json: 4.0.0 - pify: 3.0.0 - strip-bom: 3.0.0 - dev: true - /load-yaml-file@0.2.0: resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} engines: {node: '>=6'} @@ -9110,11 +9082,6 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} - /memorystream@0.3.1: - resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} - engines: {node: '>= 0.10.0'} - dev: true - /meow@6.1.1: resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==} engines: {node: '>=8'} @@ -9321,10 +9288,6 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: false - /nice-try@1.0.5: - resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} - dev: true - /node-abi@3.47.0: resolution: {integrity: sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==} engines: {node: '>=10'} @@ -9431,22 +9394,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /npm-run-all@4.1.5: - resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==} - engines: {node: '>= 4'} - hasBin: true - dependencies: - ansi-styles: 3.2.1 - chalk: 2.4.2 - cross-spawn: 6.0.5 - memorystream: 0.3.1 - minimatch: 3.1.2 - pidtree: 0.3.1 - read-pkg: 3.0.0 - shell-quote: 1.8.1 - string.prototype.padend: 3.1.4 - dev: true - /npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -9625,14 +9572,6 @@ packages: callsites: 3.1.0 dev: true - /parse-json@4.0.0: - resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} - engines: {node: '>=4'} - dependencies: - error-ex: 1.3.2 - json-parse-better-errors: 1.0.2 - dev: true - /parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -9661,11 +9600,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /path-key@2.0.1: - resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} - engines: {node: '>=4'} - dev: true - /path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -9678,13 +9612,6 @@ packages: /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} - /path-type@3.0.0: - resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} - engines: {node: '>=4'} - dependencies: - pify: 3.0.0 - dev: true - /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -9752,17 +9679,6 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - /pidtree@0.3.1: - resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==} - engines: {node: '>=0.10'} - hasBin: true - dev: true - - /pify@3.0.0: - resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} - engines: {node: '>=4'} - dev: true - /pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} @@ -10067,15 +9983,6 @@ packages: type-fest: 0.8.1 dev: true - /read-pkg@3.0.0: - resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} - engines: {node: '>=4'} - dependencies: - load-json-file: 4.0.0 - normalize-package-data: 2.5.0 - path-type: 3.0.0 - dev: true - /read-pkg@5.2.0: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} @@ -10290,8 +10197,8 @@ packages: dev: false optional: true - /safe-array-concat@1.0.0: - resolution: {integrity: sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==} + /safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} engines: {node: '>=0.4'} dependencies: call-bind: 1.0.2 @@ -10423,10 +10330,6 @@ packages: engines: {node: '>=8'} dev: true - /shell-quote@1.8.1: - resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} - dev: true - /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: @@ -10618,15 +10521,6 @@ packages: strip-ansi: 6.0.1 dev: true - /string.prototype.padend@3.1.4: - resolution: {integrity: sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 - dev: true - /string.prototype.trim@1.2.7: resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} engines: {node: '>= 0.4'} @@ -10644,8 +10538,8 @@ packages: es-abstract: 1.22.1 dev: true - /string.prototype.trimstart@1.0.6: - resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} dependencies: call-bind: 1.0.2 define-properties: 1.2.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 985ef666321..ceb38b3a8f3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,3 @@ packages: - - "services/*" - - "packages/*" + - 'services/*' + - 'packages/*' diff --git a/services/bsky/indexer.js b/services/bsky/indexer.js index 6cd390c9f43..7ab287133df 100644 --- a/services/bsky/indexer.js +++ b/services/bsky/indexer.js @@ -43,7 +43,12 @@ const main = async () => { password: cfg.redisPassword, }, ) - const indexer = BskyIndexer.create({ db, redis, cfg, imgInvalidator: cfInvalidator }) + const indexer = BskyIndexer.create({ + db, + redis, + cfg, + imgInvalidator: cfInvalidator, + }) await indexer.start() process.on('SIGTERM', async () => { await indexer.destroy() diff --git a/update-main-to-dist.js b/update-main-to-dist.js index 9ae0924bddf..fe625caa4d6 100644 --- a/update-main-to-dist.js +++ b/update-main-to-dist.js @@ -5,7 +5,5 @@ const [dir] = process.argv.slice(2) pkgJson .load(path.resolve(__dirname, dir)) - .then((pkg) => - pkg.update({ main: pkg.content.publishConfig.main }) - ) + .then((pkg) => pkg.update({ main: pkg.content.publishConfig.main })) .then((pkg) => pkg.save()) From 15411f274b20be138c0a96bc1d7d8cb482127ffc Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 7 Sep 2023 14:10:38 -0500 Subject: [PATCH 221/237] maintain feed order (#1559) * maintain feed order * clean up order, protect against undefined values * format --- packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts b/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts index 23bf349f53e..25ff4dc8f8d 100644 --- a/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts +++ b/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts @@ -14,16 +14,13 @@ export default function (server: Server, ctx: AppContext) { .orderBy('suggested_feed.order', 'asc') .selectAll() .execute() - const genInfos = await feedService.getFeedGeneratorInfos( feedsRes.map((r) => r.uri), viewer, ) - const genList = Object.values(genInfos) - + const genList = feedsRes.map((r) => genInfos[r.uri]).filter(Boolean) const creators = genList.map((gen) => gen.creator) const profiles = await feedService.getActorInfos(creators, viewer) - const feedViews = genList.map((gen) => feedService.views.formatFeedGeneratorView(gen, profiles), ) From 192392c5db817cda27dedccbecffd4c0e5203d94 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 7 Sep 2023 15:15:16 -0500 Subject: [PATCH 222/237] enable granular perms for publish action (#1563) --- .github/workflows/publish.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index ba7febd4b07..3b3b008a141 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -13,6 +13,9 @@ jobs: build: name: Build & Publish runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write steps: - uses: actions/checkout@v3 - uses: pnpm/action-setup@v2 From 44b721b6ae37fcaf4199d771ffdfb18fd57a113a Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 8 Sep 2023 10:35:49 -0700 Subject: [PATCH 223/237] Add personal-details user preference with birth date (#1565) * Add personal-details user preference with birth date * Add personal details pref to settings union --- lexicons/app/bsky/actor/defs.json | 17 ++++++++++++++++- packages/api/src/client/lexicons.ts | 11 +++++++++++ .../src/client/types/app/bsky/actor/defs.ts | 19 +++++++++++++++++++ packages/bsky/src/lexicon/lexicons.ts | 11 +++++++++++ .../src/lexicon/types/app/bsky/actor/defs.ts | 19 +++++++++++++++++++ packages/pds/src/lexicon/lexicons.ts | 11 +++++++++++ .../src/lexicon/types/app/bsky/actor/defs.ts | 19 +++++++++++++++++++ 7 files changed, 106 insertions(+), 1 deletion(-) diff --git a/lexicons/app/bsky/actor/defs.json b/lexicons/app/bsky/actor/defs.json index bd321aa60ed..6b6017d049d 100644 --- a/lexicons/app/bsky/actor/defs.json +++ b/lexicons/app/bsky/actor/defs.json @@ -94,7 +94,12 @@ "type": "array", "items": { "type": "union", - "refs": ["#adultContentPref", "#contentLabelPref", "#savedFeedsPref"] + "refs": [ + "#adultContentPref", + "#contentLabelPref", + "#savedFeedsPref", + "#personalDetailsPref" + ] } }, "adultContentPref": { @@ -134,6 +139,16 @@ } } } + }, + "personalDetailsPref": { + "type": "object", + "properties": { + "birthDate": { + "type": "string", + "format": "datetime", + "description": "The birth date of the owner of the account." + } + } } } } diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index e15d7aba1ce..193b9c39d37 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -3714,6 +3714,7 @@ export const schemaDict = { 'lex:app.bsky.actor.defs#adultContentPref', 'lex:app.bsky.actor.defs#contentLabelPref', 'lex:app.bsky.actor.defs#savedFeedsPref', + 'lex:app.bsky.actor.defs#personalDetailsPref', ], }, }, @@ -3760,6 +3761,16 @@ export const schemaDict = { }, }, }, + personalDetailsPref: { + type: 'object', + properties: { + birthDate: { + type: 'string', + format: 'datetime', + description: 'The birth date of the owner of the account.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { diff --git a/packages/api/src/client/types/app/bsky/actor/defs.ts b/packages/api/src/client/types/app/bsky/actor/defs.ts index f9ab03dcb81..7d3c9fcaac6 100644 --- a/packages/api/src/client/types/app/bsky/actor/defs.ts +++ b/packages/api/src/client/types/app/bsky/actor/defs.ts @@ -108,6 +108,7 @@ export type Preferences = ( | AdultContentPref | ContentLabelPref | SavedFeedsPref + | PersonalDetailsPref | { $type: string; [k: string]: unknown } )[] @@ -163,3 +164,21 @@ export function isSavedFeedsPref(v: unknown): v is SavedFeedsPref { export function validateSavedFeedsPref(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#savedFeedsPref', v) } + +export interface PersonalDetailsPref { + /** The birth date of the owner of the account. */ + 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) +} diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index e15d7aba1ce..193b9c39d37 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -3714,6 +3714,7 @@ export const schemaDict = { 'lex:app.bsky.actor.defs#adultContentPref', 'lex:app.bsky.actor.defs#contentLabelPref', 'lex:app.bsky.actor.defs#savedFeedsPref', + 'lex:app.bsky.actor.defs#personalDetailsPref', ], }, }, @@ -3760,6 +3761,16 @@ export const schemaDict = { }, }, }, + personalDetailsPref: { + type: 'object', + properties: { + birthDate: { + type: 'string', + format: 'datetime', + description: 'The birth date of the owner of the account.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { diff --git a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts index 3b338c06f3a..4446c1f7a03 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/actor/defs.ts @@ -108,6 +108,7 @@ export type Preferences = ( | AdultContentPref | ContentLabelPref | SavedFeedsPref + | PersonalDetailsPref | { $type: string; [k: string]: unknown } )[] @@ -163,3 +164,21 @@ export function isSavedFeedsPref(v: unknown): v is SavedFeedsPref { export function validateSavedFeedsPref(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#savedFeedsPref', v) } + +export interface PersonalDetailsPref { + /** The birth date of the owner of the account. */ + 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) +} diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index e15d7aba1ce..193b9c39d37 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -3714,6 +3714,7 @@ export const schemaDict = { 'lex:app.bsky.actor.defs#adultContentPref', 'lex:app.bsky.actor.defs#contentLabelPref', 'lex:app.bsky.actor.defs#savedFeedsPref', + 'lex:app.bsky.actor.defs#personalDetailsPref', ], }, }, @@ -3760,6 +3761,16 @@ export const schemaDict = { }, }, }, + personalDetailsPref: { + type: 'object', + properties: { + birthDate: { + type: 'string', + format: 'datetime', + description: 'The birth date of the owner of the account.', + }, + }, + }, }, }, AppBskyActorGetPreferences: { diff --git a/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts b/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts index 3b338c06f3a..4446c1f7a03 100644 --- a/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts +++ b/packages/pds/src/lexicon/types/app/bsky/actor/defs.ts @@ -108,6 +108,7 @@ export type Preferences = ( | AdultContentPref | ContentLabelPref | SavedFeedsPref + | PersonalDetailsPref | { $type: string; [k: string]: unknown } )[] @@ -163,3 +164,21 @@ export function isSavedFeedsPref(v: unknown): v is SavedFeedsPref { export function validateSavedFeedsPref(v: unknown): ValidationResult { return lexicons.validate('app.bsky.actor.defs#savedFeedsPref', v) } + +export interface PersonalDetailsPref { + /** The birth date of the owner of the account. */ + 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) +} From d1df1f323271f6e3822ef618c791abb67cb576ad Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 8 Sep 2023 10:37:24 -0700 Subject: [PATCH 224/237] @atproto/api@0.6.11 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index dad7c689fbd..5d20b27b01a 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.10", + "version": "0.6.11", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", From f8f2d04c1d43917b98786ce3a8b577bc2238379a Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Fri, 8 Sep 2023 15:08:27 -0500 Subject: [PATCH 225/237] Move fuzzy matcher to appview (#1566) * impl on appview * tests + clean up pds * tidy cfg --- packages/bsky/src/auto-moderator/abyss.ts | 4 +- .../src/auto-moderator/fuzzy-matcher.ts} | 22 ++++- packages/bsky/src/auto-moderator/index.ts | 71 +++++++++++++-- packages/bsky/src/auto-moderator/util.ts | 79 +++++++++++----- packages/bsky/src/indexer/config.ts | 15 ++++ packages/bsky/src/services/indexing/index.ts | 4 + .../auto-moderator/fuzzy-matcher.test.ts} | 53 ++++++++--- .../tests/auto-moderator/takedowns.test.ts | 6 +- packages/dev-env/src/bsky.ts | 3 + packages/dev-env/src/types.ts | 2 + .../api/com/atproto/identity/updateHandle.ts | 2 - .../api/com/atproto/server/createAccount.ts | 2 - packages/pds/src/config.ts | 19 ---- packages/pds/src/content-reporter/index.ts | 89 ------------------- packages/pds/src/context.ts | 6 -- .../explicit-slurs.ts | 0 packages/pds/src/handle/index.ts | 2 +- packages/pds/src/index.ts | 21 ----- packages/pds/src/repo/prepare.ts | 2 +- packages/pds/src/services/index.ts | 4 - packages/pds/src/services/repo/index.ts | 6 -- packages/pds/tests/handle-validation.test.ts | 22 ----- 22 files changed, 216 insertions(+), 218 deletions(-) rename packages/{pds/src/content-reporter/validator.ts => bsky/src/auto-moderator/fuzzy-matcher.ts} (83%) rename packages/{pds/tests/content-reporter.test.ts => bsky/tests/auto-moderator/fuzzy-matcher.test.ts} (67%) delete mode 100644 packages/pds/src/content-reporter/index.ts rename packages/pds/src/{content-reporter => handle}/explicit-slurs.ts (100%) diff --git a/packages/bsky/src/auto-moderator/abyss.ts b/packages/bsky/src/auto-moderator/abyss.ts index 6d9cb6afa37..fb9ee2c4e98 100644 --- a/packages/bsky/src/auto-moderator/abyss.ts +++ b/packages/bsky/src/auto-moderator/abyss.ts @@ -7,11 +7,11 @@ import { PrimaryDatabase } from '../db' import { IdResolver } from '@atproto/identity' import { labelerLogger as log } from '../logger' -export interface TakedownFlagger { +export interface ImageFlagger { scanImage(did: string, cid: CID): Promise } -export class Abyss implements TakedownFlagger { +export class Abyss implements ImageFlagger { protected auth: string constructor( diff --git a/packages/pds/src/content-reporter/validator.ts b/packages/bsky/src/auto-moderator/fuzzy-matcher.ts similarity index 83% rename from packages/pds/src/content-reporter/validator.ts rename to packages/bsky/src/auto-moderator/fuzzy-matcher.ts index 9f5b5689e4a..07b5fb9a85e 100644 --- a/packages/pds/src/content-reporter/validator.ts +++ b/packages/bsky/src/auto-moderator/fuzzy-matcher.ts @@ -1,6 +1,11 @@ import { dedupeStrs } from '@atproto/common' +import * as ui8 from 'uint8arrays' -export class UnacceptableWordValidator { +export interface TextFlagger { + getMatches(string: string): string[] +} + +export class FuzzyMatcher implements TextFlagger { private bannedWords: Set private falsePositives: Set @@ -11,6 +16,13 @@ export class UnacceptableWordValidator { ) } + static fromB64(bannedB64: string, falsePositivesB64?: string) { + return new FuzzyMatcher( + decode(bannedB64), + falsePositivesB64 ? decode(falsePositivesB64) : undefined, + ) + } + private normalize(domain: string): string[] { const withoutSymbols = domain.replace(/[\W_]+/g, '') // Remove non-alphanumeric characters const lowercase = withoutSymbols.toLowerCase() @@ -104,3 +116,11 @@ export class UnacceptableWordValidator { return [] } } + +export const decode = (encoded: string): string[] => { + return ui8.toString(ui8.fromString(encoded, 'base64'), 'utf8').split(',') +} + +export const encode = (words: string[]): string => { + return ui8.toString(ui8.fromString(words.join(','), 'utf8'), 'base64') +} diff --git a/packages/bsky/src/auto-moderator/index.ts b/packages/bsky/src/auto-moderator/index.ts index a2d041d17f3..85cc529bce1 100644 --- a/packages/bsky/src/auto-moderator/index.ts +++ b/packages/bsky/src/auto-moderator/index.ts @@ -10,17 +10,20 @@ import { buildBasicAuth } from '../auth' import { CID } from 'multiformats/cid' import { LabelService } from '../services/label' import { ModerationService } from '../services/moderation' -import { TakedownFlagger } from './abyss' +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 { REASONOTHER } from '../lexicon/types/com/atproto/moderation/defs' export class AutoModerator { public pushAgent?: AtpAgent - public takedownFlagger?: TakedownFlagger + public imageFlagger?: ImageFlagger + public textFlagger?: TextFlagger public imgLabeler?: ImgLabeler public textLabeler?: TextLabeler @@ -59,7 +62,7 @@ export class AutoModerator { this.imgLabeler = hiveApiKey ? new HiveLabeler(hiveApiKey, ctx) : undefined this.textLabeler = new KeywordLabeler(ctx.cfg.labelerKeywords) if (abyssEndpoint && abyssPassword) { - this.takedownFlagger = new Abyss(abyssEndpoint, abyssPassword, ctx) + this.imageFlagger = new Abyss(abyssEndpoint, abyssPassword, ctx) } else { log.error( { abyssEndpoint, abyssPassword }, @@ -67,6 +70,13 @@ export class AutoModerator { ) } + if (ctx.cfg.fuzzyMatchB64) { + this.textFlagger = FuzzyMatcher.fromB64( + ctx.cfg.fuzzyMatchB64, + ctx.cfg.fuzzyFalsePositiveB64, + ) + } + if (ctx.cfg.moderationPushUrl) { const url = new URL(ctx.cfg.moderationPushUrl) this.pushAgent = new AtpAgent({ service: url.origin }) @@ -79,7 +89,7 @@ export class AutoModerator { processRecord(uri: AtUri, cid: CID, obj: unknown) { this.ctx.backgroundQueue.add(async () => { - const { text, imgs } = getFieldsFromRecord(obj) + const { text, imgs } = getFieldsFromRecord(obj, uri) await Promise.all([ this.labelRecord(uri, cid, text, imgs).catch((err) => { log.error( @@ -87,6 +97,12 @@ export class AutoModerator { 'failed to label record', ) }), + this.flagRecordText(uri, cid, text).catch((err) => { + log.error( + { err, uri: uri.toString(), record: obj }, + 'failed to check record for text flagging', + ) + }), this.checkImgForTakedown(uri, cid, imgs).catch((err) => { log.error( { err, uri: uri.toString(), record: obj }, @@ -97,8 +113,16 @@ export class AutoModerator { }) } + processHandle(handle: string, did: string) { + this.ctx.backgroundQueue.add(async () => { + await this.flagSubjectText(handle, { did }).catch((err) => { + log.error({ err, handle, did }, 'failed to label handle') + }) + }) + } + async labelRecord(uri: AtUri, recordCid: CID, text: string[], imgs: CID[]) { - if (uri.collection === ids.AppBskyActorProfile) { + if (uri.collection !== ids.AppBskyFeedPost) { // @TODO label profiles return } @@ -110,10 +134,45 @@ export class AutoModerator { await this.storeLabels(uri, recordCid, labels) } + async flagRecordText(uri: AtUri, cid: CID, text: string[]) { + if ( + ![ + ids.AppBskyActorProfile, + ids.AppBskyGraphList, + ids.AppBskyFeedGenerator, + ].includes(uri.collection) + ) { + return + } + return this.flagSubjectText(text.join(' '), { uri, cid }) + } + + async flagSubjectText( + text: string, + subject: { did: string } | { uri: AtUri; cid: CID }, + ) { + if (!this.textFlagger) return + const matches = this.textFlagger.getMatches(text) + if (matches.length < 1) return + if (!this.services.moderation) { + log.error( + { subject, text, matches }, + 'no moderation service setup to flag record text', + ) + return + } + await this.services.moderation(this.ctx.db).report({ + reasonType: REASONOTHER, + reason: `Automatically flagged for possible slurs: ${matches.join(', ')}`, + subject, + reportedBy: this.ctx.cfg.labelerDid, + }) + } + async checkImgForTakedown(uri: AtUri, recordCid: CID, imgCids: CID[]) { if (imgCids.length < 0) return const results = await Promise.all( - imgCids.map((cid) => this.takedownFlagger?.scanImage(uri.host, cid)), + imgCids.map((cid) => this.imageFlagger?.scanImage(uri.host, cid)), ) const takedownCids: CID[] = [] for (let i = 0; i < results.length; i++) { diff --git a/packages/bsky/src/auto-moderator/util.ts b/packages/bsky/src/auto-moderator/util.ts index ba49eb2a9f3..ab1467a07f2 100644 --- a/packages/bsky/src/auto-moderator/util.ts +++ b/packages/bsky/src/auto-moderator/util.ts @@ -1,7 +1,22 @@ import { CID } from 'multiformats/cid' +import { AtUri } from '@atproto/syntax' import * as lex from '../lexicon/lexicons' -import { Record as PostRecord } from '../lexicon/types/app/bsky/feed/post' -import { Record as ProfileRecord } from '../lexicon/types/app/bsky/actor/profile' +import { + isRecord as isPost, + Record as PostRecord, +} from '../lexicon/types/app/bsky/feed/post' +import { + isRecord as isProfile, + Record as ProfileRecord, +} from '../lexicon/types/app/bsky/actor/profile' +import { + isRecord as isList, + Record as ListRecord, +} from '../lexicon/types/app/bsky/graph/list' +import { + isRecord as isGenerator, + Record as GeneratorRecord, +} from '../lexicon/types/app/bsky/feed/generator' import { isMain as isEmbedImage } from '../lexicon/types/app/bsky/embed/images' import { isMain as isEmbedExternal } from '../lexicon/types/app/bsky/embed/external' import { isMain as isEmbedRecordWithMedia } from '../lexicon/types/app/bsky/embed/recordWithMedia' @@ -11,11 +26,18 @@ type RecordFields = { imgs: CID[] } -export const getFieldsFromRecord = (record: unknown): RecordFields => { +export const getFieldsFromRecord = ( + record: unknown, + uri: AtUri, +): RecordFields => { if (isPost(record)) { return getFieldsFromPost(record) } else if (isProfile(record)) { return getFieldsFromProfile(record) + } else if (isList(record)) { + return getFieldsFromList(record) + } else if (isGenerator(record)) { + return getFieldsFromGenerator(record, uri) } else { return { text: [], imgs: [] } } @@ -61,6 +83,40 @@ export const getFieldsFromProfile = (record: ProfileRecord): RecordFields => { return { text, imgs } } +export const getFieldsFromList = (record: ListRecord): RecordFields => { + const text: string[] = [] + const imgs: CID[] = [] + if (record.name) { + text.push(record.name) + } + if (record.description) { + text.push(record.description) + } + if (record.avatar) { + imgs.push(record.avatar.ref) + } + return { text, imgs } +} + +export const getFieldsFromGenerator = ( + record: GeneratorRecord, + uri: AtUri, +): RecordFields => { + const text: string[] = [] + const imgs: CID[] = [] + text.push(uri.rkey) + if (record.displayName) { + text.push(record.displayName) + } + if (record.description) { + text.push(record.description) + } + if (record.avatar) { + imgs.push(record.avatar.ref) + } + return { text, imgs } +} + export const dedupe = (strs: (string | undefined)[]): string[] => { const set = new Set() for (const str of strs) { @@ -71,23 +127,6 @@ export const dedupe = (strs: (string | undefined)[]): string[] => { return [...set] } -export const isPost = (obj: unknown): obj is PostRecord => { - return isRecordType(obj, 'app.bsky.feed.post') -} - -export const isProfile = (obj: unknown): obj is ProfileRecord => { - return isRecordType(obj, 'app.bsky.actor.profile') -} - -export const isRecordType = (obj: unknown, lexId: string): boolean => { - try { - lex.lexicons.assertValidRecord(lexId, obj) - return true - } catch { - return false - } -} - const separateEmbeds = (embed: PostRecord['embed']) => { if (!embed) { return [] diff --git a/packages/bsky/src/indexer/config.ts b/packages/bsky/src/indexer/config.ts index 579cfd24432..dd8b9ab89d5 100644 --- a/packages/bsky/src/indexer/config.ts +++ b/packages/bsky/src/indexer/config.ts @@ -18,6 +18,8 @@ export interface IndexerConfigValues { abyssEndpoint?: string abyssPassword?: string imgUriEndpoint?: string + fuzzyMatchB64?: string + fuzzyFalsePositiveB64?: string labelerKeywords: Record moderationPushUrl?: string indexerConcurrency?: number @@ -90,6 +92,9 @@ export class IndexerConfig { const ingesterPartitionCount = maybeParseInt(process.env.INGESTER_PARTITION_COUNT) ?? 64 const labelerKeywords = {} + const fuzzyMatchB64 = process.env.FUZZY_MATCH_B64 || undefined + const fuzzyFalsePositiveB64 = + process.env.FUZZY_FALSE_POSITIVE_B64 || undefined const pushNotificationEndpoint = process.env.PUSH_NOTIFICATION_ENDPOINT assert(dbPostgresUrl) assert(redisHost || (redisSentinelName && redisSentinelHosts?.length)) @@ -120,6 +125,8 @@ export class IndexerConfig { indexerPort, ingesterPartitionCount, labelerKeywords, + fuzzyMatchB64, + fuzzyFalsePositiveB64, pushNotificationEndpoint, ...stripUndefineds(overrides ?? {}), }) @@ -225,6 +232,14 @@ export class IndexerConfig { return this.cfg.labelerKeywords } + get fuzzyMatchB64() { + return this.cfg.fuzzyMatchB64 + } + + get fuzzyFalsePositiveB64() { + return this.cfg.fuzzyFalsePositiveB64 + } + get pushNotificationEndpoint() { return this.cfg.pushNotificationEndpoint } diff --git a/packages/bsky/src/services/indexing/index.ts b/packages/bsky/src/services/indexing/index.ts index ff979ee034f..33f0a2577c2 100644 --- a/packages/bsky/src/services/indexing/index.ts +++ b/packages/bsky/src/services/indexing/index.ts @@ -165,6 +165,10 @@ export class IndexingService { .onConflict((oc) => oc.column('did').doUpdateSet(actorInfo)) .returning('did') .executeTakeFirst() + + if (handle) { + this.autoMod.processHandle(handle, did) + } } async indexRepo(did: string, commit?: string) { diff --git a/packages/pds/tests/content-reporter.test.ts b/packages/bsky/tests/auto-moderator/fuzzy-matcher.test.ts similarity index 67% rename from packages/pds/tests/content-reporter.test.ts rename to packages/bsky/tests/auto-moderator/fuzzy-matcher.test.ts index e4a25d68f05..f9fc320dcb9 100644 --- a/packages/pds/tests/content-reporter.test.ts +++ b/packages/bsky/tests/auto-moderator/fuzzy-matcher.test.ts @@ -1,23 +1,29 @@ -import { encode } from '../src/content-reporter' -import { TestNetworkNoAppView } from '@atproto/dev-env' -import { SeedClient } from './seeds/client' -import basicSeed from './seeds/basic' +import { FuzzyMatcher, encode } from '../../src/auto-moderator/fuzzy-matcher' +import { TestNetwork } from '@atproto/dev-env' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' import { AtpAgent } from '@atproto/api' +import { ImageInvalidator } from '../../src/image/invalidator' -describe('content reporter', () => { - let network: TestNetworkNoAppView +describe('fuzzy matcher', () => { + let network: TestNetwork let agent: AtpAgent let sc: SeedClient + let fuzzyMatcher: FuzzyMatcher let alice: string beforeAll(async () => { - network = await TestNetworkNoAppView.create({ - dbPostgresSchema: 'content_reporter', - pds: { - unacceptableWordsB64: encode(['evil']), + network = await TestNetwork.create({ + dbPostgresSchema: 'fuzzy_matcher', + bsky: { + imgInvalidator: new NoopInvalidator(), + indexer: { + fuzzyMatchB64: encode(['evil']), + }, }, }) + fuzzyMatcher = new FuzzyMatcher(['evil', 'mean', 'bad'], ['baddie']) agent = network.pds.getClient() sc = new SeedClient(agent) await basicSeed(sc) @@ -30,13 +36,30 @@ describe('content reporter', () => { }) const getAllReports = () => { - return network.pds.ctx.db.db - .selectFrom('moderation_report') + return network.bsky.ctx.db + .getPrimary() + .db.selectFrom('moderation_report') .selectAll() .orderBy('id', 'asc') .execute() } + it('identifies fuzzy matches', () => { + expect(fuzzyMatcher.getMatches('evil.john.test')).toMatchObject(['evil']) + expect(fuzzyMatcher.getMatches('john.evil.test')).toMatchObject(['evil']) + expect(fuzzyMatcher.getMatches('john.test.evil')).toMatchObject(['evil']) + expect(fuzzyMatcher.getMatches('ev1l.test.john')).toMatchObject(['evil']) + expect(fuzzyMatcher.getMatches('ev-1l.test.john')).toMatchObject(['evil']) + expect(fuzzyMatcher.getMatches('ev-11.test.john')).toMatchObject(['evil']) + expect(fuzzyMatcher.getMatches('ev.-1.l-test.john')).toMatchObject(['evil']) + }) + + it('identifies fuzzy false positivies', () => { + expect(fuzzyMatcher.getMatches('john.test')).toHaveLength(0) + expect(fuzzyMatcher.getMatches('good.john.test')).toHaveLength(0) + expect(fuzzyMatcher.getMatches('john.baddie.test')).toHaveLength(0) + }) + it('doesnt label any of the content in the seed', async () => { const reports = await getAllReports() expect(reports.length).toBe(0) @@ -62,7 +85,7 @@ describe('content reporter', () => { }, { headers: sc.getHeaders(alice), encoding: 'application/json' }, ) - await network.pds.ctx.backgroundQueue.processAll() + await network.processAll() const reports = await getAllReports() expect(reports.length).toBe(2) @@ -136,3 +159,7 @@ describe('content reporter', () => { expect(reports.at(-1)?.subjectCid).toEqual(res.data.cid) }) }) + +class NoopInvalidator implements ImageInvalidator { + async invalidate() {} +} diff --git a/packages/bsky/tests/auto-moderator/takedowns.test.ts b/packages/bsky/tests/auto-moderator/takedowns.test.ts index 0fa4cbf730c..27b0c986115 100644 --- a/packages/bsky/tests/auto-moderator/takedowns.test.ts +++ b/packages/bsky/tests/auto-moderator/takedowns.test.ts @@ -7,7 +7,7 @@ import { TestNetwork } from '@atproto/dev-env' import { ImageRef, SeedClient } from '../seeds/client' import usersSeed from '../seeds/users' import { CID } from 'multiformats/cid' -import { TakedownFlagger } from '../../src/auto-moderator/abyss' +import { ImageFlagger } from '../../src/auto-moderator/abyss' import { ImageInvalidator } from '../../src/image/invalidator' import { sha256 } from '@atproto/crypto' import { ids } from '../../src/lexicon/lexicons' @@ -38,7 +38,7 @@ describe('takedowner', () => { }) ctx = network.bsky.indexer.ctx autoMod = ctx.autoMod - autoMod.takedownFlagger = new TestFlagger() + autoMod.imageFlagger = new TestFlagger() pdsAgent = new AtpAgent({ service: network.pds.url }) sc = new SeedClient(pdsAgent) await usersSeed(sc) @@ -156,7 +156,7 @@ class TestInvalidator implements ImageInvalidator { } } -class TestFlagger implements TakedownFlagger { +class TestFlagger implements ImageFlagger { async scanImage(_did: string, cid: CID): Promise { if (cid.equals(badCid1)) { return ['kill-it'] diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index 15ea03375e2..a99385b755b 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -95,6 +95,7 @@ export class TestBsky { labelerKeywords: { label_me: 'test-label', label_me_2: 'test-label-2' }, abyssEndpoint: '', abyssPassword: '', + imgUriEndpoint: 'img.example.com', moderationPushUrl: `http://admin:${config.adminPassword}@localhost:${cfg.pdsPort}`, indexerPartitionIds: [0], indexerNamespace: `ns${ns}`, @@ -102,6 +103,7 @@ export class TestBsky { indexerPort: await getPort(), ingesterPartitionCount: 1, pushNotificationEndpoint: 'https://push.bsky.app/api/push', + ...(cfg.indexer ?? {}), }) assert(indexerCfg.redisHost) const indexerRedis = new bsky.Redis({ @@ -124,6 +126,7 @@ export class TestBsky { ingesterNamespace: `ns${ns}`, ingesterSubLockId: uniqueLockId(), ingesterPartitionCount: 1, + ...(cfg.ingester ?? {}), }) assert(ingesterCfg.redisHost) const ingesterRedis = new bsky.Redis({ diff --git a/packages/dev-env/src/types.ts b/packages/dev-env/src/types.ts index ef8f357ee73..0aac4f3aa25 100644 --- a/packages/dev-env/src/types.ts +++ b/packages/dev-env/src/types.ts @@ -24,6 +24,8 @@ export type BskyConfig = Partial & { imgInvalidator?: ImageInvalidator migration?: string algos?: bsky.MountedAlgos + indexer?: Partial + ingester?: Partial } export type TestServerParams = { diff --git a/packages/pds/src/api/com/atproto/identity/updateHandle.ts b/packages/pds/src/api/com/atproto/identity/updateHandle.ts index ccf8e56b1bd..6db63fab0c0 100644 --- a/packages/pds/src/api/com/atproto/identity/updateHandle.ts +++ b/packages/pds/src/api/com/atproto/identity/updateHandle.ts @@ -73,8 +73,6 @@ export default function (server: Server, ctx: AppContext) { 'failed to sequence handle update', ) } - - ctx.contentReporter?.checkHandle({ handle, did: requester }) }, }) } diff --git a/packages/pds/src/api/com/atproto/server/createAccount.ts b/packages/pds/src/api/com/atproto/server/createAccount.ts index 313f7ab5cd8..5827ff6c658 100644 --- a/packages/pds/src/api/com/atproto/server/createAccount.ts +++ b/packages/pds/src/api/com/atproto/server/createAccount.ts @@ -111,8 +111,6 @@ export default function (server: Server, ctx: AppContext) { } }) - ctx.contentReporter?.checkHandle({ handle, did: result.did }) - return { encoding: 'application/json', body: { diff --git a/packages/pds/src/config.ts b/packages/pds/src/config.ts index 741fae22fde..c6a176bfe35 100644 --- a/packages/pds/src/config.ts +++ b/packages/pds/src/config.ts @@ -57,8 +57,6 @@ export interface ServerConfigValues { hiveApiKey?: string labelerDid: string labelerKeywords: Record - unacceptableWordsB64?: string - falsePositiveWordsB64?: string feedGenDid?: string @@ -189,13 +187,6 @@ export class ServerConfig { const labelerDid = process.env.LABELER_DID || 'did:example:labeler' const labelerKeywords = {} - const unacceptableWordsB64 = nonemptyString( - process.env.UNACCEPTABLE_WORDS_B64, - ) - const falsePositiveWordsB64 = nonemptyString( - process.env.FALSE_POSITIVE_WORDS_B64, - ) - const feedGenDid = process.env.FEED_GEN_DID const dbPostgresUrl = process.env.DB_POSTGRES_URL @@ -285,8 +276,6 @@ export class ServerConfig { hiveApiKey, labelerDid, labelerKeywords, - unacceptableWordsB64, - falsePositiveWordsB64, feedGenDid, maxSubscriptionBuffer, repoBackfillLimitMs, @@ -505,14 +494,6 @@ export class ServerConfig { return this.cfg.labelerKeywords } - get unacceptableWordsB64() { - return this.cfg.unacceptableWordsB64 - } - - get falsePositiveWordsB64() { - return this.cfg.falsePositiveWordsB64 - } - get feedGenDid() { return this.cfg.feedGenDid } diff --git a/packages/pds/src/content-reporter/index.ts b/packages/pds/src/content-reporter/index.ts deleted file mode 100644 index 0f21fa6986f..00000000000 --- a/packages/pds/src/content-reporter/index.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { AtUri } from '@atproto/syntax' -import { RepoRecord } from '@atproto/lexicon' -import { CID } from 'multiformats/cid' -import * as ui8 from 'uint8arrays' -import { UnacceptableWordValidator } from './validator' -import { REASONOTHER } from '../lexicon/types/com/atproto/moderation/defs' -import { isRecord as isList } from '../lexicon/types/app/bsky/graph/list' -import { isRecord as isProfile } from '../lexicon/types/app/bsky/actor/profile' -import { isRecord as isFeedGenerator } from '../lexicon/types/app/bsky/feed/generator' -import { BackgroundQueue } from '../event-stream/background-queue' -import { ModerationService } from '../services/moderation' - -export class ContentReporter { - backgroundQueue: BackgroundQueue - moderationService: ModerationService - reporterDid: string - validator: UnacceptableWordValidator - - constructor(opts: { - backgroundQueue: BackgroundQueue - moderationService: ModerationService - reporterDid: string - unacceptableB64: string - falsePositivesB64?: string - }) { - this.backgroundQueue = opts.backgroundQueue - this.moderationService = opts.moderationService - this.reporterDid = opts.reporterDid - this.validator = new UnacceptableWordValidator( - decode(opts.unacceptableB64), - opts.falsePositivesB64 ? decode(opts.falsePositivesB64) : undefined, - ) - } - - checkHandle(opts: { handle: string; did: string }) { - const { handle, did } = opts - return this.checkContent({ - content: handle, - subject: { did }, - }) - } - - checkRecord(opts: { record: RepoRecord; uri: AtUri; cid: CID }) { - const { record, uri, cid } = opts - let content = '' - if (isProfile(record)) { - content += ' ' + record.displayName - } else if (isList(record)) { - content += ' ' + record.name - } else if (isFeedGenerator(record)) { - content += ' ' + uri.rkey - content += ' ' + record.displayName - } - - return this.checkContent({ - content, - subject: { uri, cid }, - }) - } - - checkContent(opts: { - content: string - subject: { did: string } | { uri: AtUri; cid?: CID } - }) { - const { content, subject } = opts - const possibleSlurs = this.validator.getMatches(content) - if (possibleSlurs.length < 1) { - return - } - this.backgroundQueue.add(async () => { - await this.moderationService.report({ - reasonType: REASONOTHER, - reason: `Automatically flagged for possible slurs: ${possibleSlurs.join( - ', ', - )}`, - subject, - reportedBy: this.reporterDid, - }) - }) - } -} - -export const decode = (encoded: string): string[] => { - return ui8.toString(ui8.fromString(encoded, 'base64'), 'utf8').split(',') -} - -export const encode = (words: string[]): string => { - return ui8.toString(ui8.fromString(words.join(','), 'utf8'), 'base64') -} diff --git a/packages/pds/src/context.ts b/packages/pds/src/context.ts index 06740ea41e5..ca57730504b 100644 --- a/packages/pds/src/context.ts +++ b/packages/pds/src/context.ts @@ -21,7 +21,6 @@ import DidSqlCache from './did-cache' import { MountedAlgos } from './feed-gen/types' import { Crawlers } from './crawlers' import { LabelCache } from './label-cache' -import { ContentReporter } from './content-reporter' import { RuntimeFlags } from './runtime-flags' export class AppContext { @@ -46,7 +45,6 @@ export class AppContext { labeler: Labeler labelCache: LabelCache runtimeFlags: RuntimeFlags - contentReporter?: ContentReporter backgroundQueue: BackgroundQueue appviewAgent?: AtpAgent crawlers: Crawlers @@ -150,10 +148,6 @@ export class AppContext { return this.opts.runtimeFlags } - get contentReporter(): ContentReporter | undefined { - return this.opts.contentReporter - } - get backgroundQueue(): BackgroundQueue { return this.opts.backgroundQueue } diff --git a/packages/pds/src/content-reporter/explicit-slurs.ts b/packages/pds/src/handle/explicit-slurs.ts similarity index 100% rename from packages/pds/src/content-reporter/explicit-slurs.ts rename to packages/pds/src/handle/explicit-slurs.ts diff --git a/packages/pds/src/handle/index.ts b/packages/pds/src/handle/index.ts index f9f05c28c48..deae5409945 100644 --- a/packages/pds/src/handle/index.ts +++ b/packages/pds/src/handle/index.ts @@ -1,7 +1,7 @@ import * as ident from '@atproto/syntax' import { InvalidRequestError } from '@atproto/xrpc-server' import { reservedSubdomains } from './reserved' -import { hasExplicitSlur } from '../content-reporter/explicit-slurs' +import { hasExplicitSlur } from './explicit-slurs' import AppContext from '../context' export const normalizeAndValidateHandle = async (opts: { diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 67552df8e3d..32abb30056d 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -52,8 +52,6 @@ import DidSqlCache from './did-cache' import { MountedAlgos } from './feed-gen/types' import { Crawlers } from './crawlers' import { LabelCache } from './label-cache' -import { ContentReporter } from './content-reporter' -import { ModerationService } from './services/moderation' import { getRedisClient } from './redis' import { RuntimeFlags } from './runtime-flags' @@ -200,23 +198,6 @@ export class PDS { const labelCache = new LabelCache(db) - let contentReporter: ContentReporter | undefined = undefined - if (config.unacceptableWordsB64) { - contentReporter = new ContentReporter({ - backgroundQueue, - moderationService: new ModerationService( - db, - messageDispatcher, - blobstore, - imgUriBuilder, - imgInvalidator, - ), - reporterDid: config.labelerDid, - unacceptableB64: config.unacceptableWordsB64, - falsePositivesB64: config.falsePositiveWordsB64, - }) - } - const appviewAgent = config.bskyAppViewEndpoint ? new AtpAgent({ service: config.bskyAppViewEndpoint }) : undefined @@ -229,7 +210,6 @@ export class PDS { imgInvalidator, labeler, labelCache, - contentReporter, appviewAgent, appviewDid: config.bskyAppViewDid, appviewCdnUrlPattern: config.bskyAppViewCdnUrlPattern, @@ -263,7 +243,6 @@ export class PDS { labeler, labelCache, runtimeFlags, - contentReporter, services, mailer, moderationMailer, diff --git a/packages/pds/src/repo/prepare.ts b/packages/pds/src/repo/prepare.ts index c93030d1e6f..60fbe2d81cd 100644 --- a/packages/pds/src/repo/prepare.ts +++ b/packages/pds/src/repo/prepare.ts @@ -33,7 +33,7 @@ import { } from '../lexicon/types/app/bsky/feed/post' import { isRecord as isList } from '../lexicon/types/app/bsky/graph/list' import { isRecord as isProfile } from '../lexicon/types/app/bsky/actor/profile' -import { hasExplicitSlur } from '../content-reporter/explicit-slurs' +import { hasExplicitSlur } from '../handle/explicit-slurs' import { InvalidRequestError } from '@atproto/xrpc-server' // @TODO do this dynamically off of schemas diff --git a/packages/pds/src/services/index.ts b/packages/pds/src/services/index.ts index 6767b1c535e..b49693cc8cf 100644 --- a/packages/pds/src/services/index.ts +++ b/packages/pds/src/services/index.ts @@ -19,7 +19,6 @@ import { LabelService } from '../app-view/services/label' import { BackgroundQueue } from '../event-stream/background-queue' import { Crawlers } from '../crawlers' import { LabelCache } from '../label-cache' -import { ContentReporter } from '../content-reporter' import { LocalService } from './local' export function createServices(resources: { @@ -30,7 +29,6 @@ export function createServices(resources: { imgInvalidator: ImageInvalidator labeler: Labeler labelCache: LabelCache - contentReporter?: ContentReporter appviewAgent?: AtpAgent appviewDid?: string appviewCdnUrlPattern?: string @@ -45,7 +43,6 @@ export function createServices(resources: { imgInvalidator, labeler, labelCache, - contentReporter, appviewAgent, appviewDid, appviewCdnUrlPattern, @@ -63,7 +60,6 @@ export function createServices(resources: { backgroundQueue, crawlers, labeler, - contentReporter, ), local: LocalService.creator( repoSigningKey, diff --git a/packages/pds/src/services/repo/index.ts b/packages/pds/src/services/repo/index.ts index 759d1c50c37..8b8db8eb6be 100644 --- a/packages/pds/src/services/repo/index.ts +++ b/packages/pds/src/services/repo/index.ts @@ -20,7 +20,6 @@ import { Labeler } from '../../labeler' import { wait } from '@atproto/common' import { BackgroundQueue } from '../../event-stream/background-queue' import { Crawlers } from '../../crawlers' -import { ContentReporter } from '../../content-reporter' export class RepoService { blobs: RepoBlobs @@ -33,7 +32,6 @@ export class RepoService { public backgroundQueue: BackgroundQueue, public crawlers: Crawlers, public labeler: Labeler, - public contentReporter?: ContentReporter, ) { this.blobs = new RepoBlobs(db, blobstore, backgroundQueue) } @@ -45,7 +43,6 @@ export class RepoService { backgroundQueue: BackgroundQueue, crawlers: Crawlers, labeler: Labeler, - contentReporter?: ContentReporter, ) { return (db: Database) => new RepoService( @@ -56,7 +53,6 @@ export class RepoService { backgroundQueue, crawlers, labeler, - contentReporter, ) } @@ -77,7 +73,6 @@ export class RepoService { this.backgroundQueue, this.crawlers, this.labeler, - this.contentReporter, ) return fn(srvc) }) @@ -306,7 +301,6 @@ export class RepoService { ) { // @TODO move to appview this.labeler.processRecord(write.uri, write.record) - this.contentReporter?.checkRecord(write) } }) }) diff --git a/packages/pds/tests/handle-validation.test.ts b/packages/pds/tests/handle-validation.test.ts index bf55d61e49f..c39f7db18de 100644 --- a/packages/pds/tests/handle-validation.test.ts +++ b/packages/pds/tests/handle-validation.test.ts @@ -1,6 +1,5 @@ import { isValidTld } from '@atproto/syntax' import { ensureHandleServiceConstraints } from '../src/handle' -import { UnacceptableWordValidator } from '../src/content-reporter/validator' describe('handle validation', () => { it('validates service constraints', () => { @@ -26,25 +25,4 @@ describe('handle validation', () => { expect(isValidTld('atproto.onion')).toBe(false) expect(isValidTld('atproto.internal')).toBe(false) }) - - const validator = new UnacceptableWordValidator( - ['evil', 'mean', 'bad'], - ['baddie'], - ) - - it('identifies offensive handles', () => { - expect(validator.getMatches('evil.john.test')).toMatchObject(['evil']) - expect(validator.getMatches('john.evil.test')).toMatchObject(['evil']) - expect(validator.getMatches('john.test.evil')).toMatchObject(['evil']) - expect(validator.getMatches('ev1l.test.john')).toMatchObject(['evil']) - expect(validator.getMatches('ev-1l.test.john')).toMatchObject(['evil']) - expect(validator.getMatches('ev-11.test.john')).toMatchObject(['evil']) - expect(validator.getMatches('ev.-1.l-test.john')).toMatchObject(['evil']) - }) - - it('identifies non-offensive handles', () => { - expect(validator.getMatches('john.test')).toHaveLength(0) - expect(validator.getMatches('good.john.test')).toHaveLength(0) - expect(validator.getMatches('john.baddie.test')).toHaveLength(0) - }) }) From b3046c348fedf7a2d62c7b0f66f71fab4440acd2 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Fri, 8 Sep 2023 15:25:30 -0500 Subject: [PATCH 226/237] Remove legacy repo sync impl (#1570) remove legacy repo sync impl --- .../atproto/sync/deprecated/getCheckout.ts | 22 ++++------- .../pds/src/api/com/atproto/sync/getRepo.ts | 4 +- packages/pds/src/sql-repo-storage.ts | 38 ------------------- 3 files changed, 9 insertions(+), 55 deletions(-) diff --git a/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts b/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts index 28e19f22840..cbde7131c66 100644 --- a/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts +++ b/packages/pds/src/api/com/atproto/sync/deprecated/getCheckout.ts @@ -6,7 +6,6 @@ import SqlRepoStorage, { } from '../../../../../sql-repo-storage' import AppContext from '../../../../../context' import { isUserOrAdmin } from '../../../../../auth' -import { getFullRepo } from '@atproto/repo' export default function (server: Server, ctx: AppContext) { server.com.atproto.sync.getCheckout({ @@ -24,20 +23,15 @@ export default function (server: Server, ctx: AppContext) { } const storage = new SqlRepoStorage(ctx.db, did) - const head = await storage.getRoot() - if (!head) { - throw new InvalidRequestError(`Could not find repo for DID: ${did}`) + let carStream: AsyncIterable + try { + carStream = await storage.getCarStream() + } catch (err) { + if (err instanceof RepoRootNotFoundError) { + throw new InvalidRequestError(`Could not find repo for DID: ${did}`) + } + throw err } - const carStream = getFullRepo(storage, head) - // let carStream: AsyncIterable - // try { - // carStream = await storage.getCarStream() - // } catch (err) { - // if (err instanceof RepoRootNotFoundError) { - // throw new InvalidRequestError(`Could not find repo for DID: ${did}`) - // } - // throw err - // } return { encoding: 'application/vnd.ipld.car', diff --git a/packages/pds/src/api/com/atproto/sync/getRepo.ts b/packages/pds/src/api/com/atproto/sync/getRepo.ts index a88a39cb144..9037a2a3a9c 100644 --- a/packages/pds/src/api/com/atproto/sync/getRepo.ts +++ b/packages/pds/src/api/com/atproto/sync/getRepo.ts @@ -25,9 +25,7 @@ export default function (server: Server, ctx: AppContext) { const storage = new SqlRepoStorage(ctx.db, did) let carStream: AsyncIterable try { - carStream = since - ? await storage.getCarStream(since) - : await storage.getCarStreamLegacy() + carStream = await storage.getCarStream(since) } catch (err) { if (err instanceof RepoRootNotFoundError) { throw new InvalidRequestError(`Could not find repo for DID: ${did}`) diff --git a/packages/pds/src/sql-repo-storage.ts b/packages/pds/src/sql-repo-storage.ts index c8fdf1297d5..a7b6a5ae1ea 100644 --- a/packages/pds/src/sql-repo-storage.ts +++ b/packages/pds/src/sql-repo-storage.ts @@ -206,44 +206,6 @@ export class SqlRepoStorage extends ReadableBlockstore implements RepoStorage { } } - async getCarStreamLegacy() { - const root = await this.getRoot() - if (!root) { - throw new RepoRootNotFoundError() - } - return writeCarStream(root, async (car) => { - let cursor: CID | undefined = undefined - do { - const res = await this.getBlockRangeLegacy(cursor) - for (const row of res) { - await car.put({ - cid: CID.parse(row.cid), - bytes: row.content, - }) - } - const lastRow = res.at(-1) - if (lastRow) { - cursor = CID.parse(lastRow.cid) - } else { - cursor = undefined - } - } while (cursor) - }) - } - - async getBlockRangeLegacy(cursor?: CID) { - let builder = this.db.db - .selectFrom('ipld_block') - .where('creator', '=', this.did) - .select(['cid', 'content']) - .orderBy('cid', 'asc') - .limit(500) - if (cursor) { - builder = builder.where('cid', '>', cursor.toString()) - } - return builder.execute() - } - async getCarStream(since?: string) { const root = await this.getRoot() if (!root) { From 453696728021af5867ba3c174691da0d376a24e8 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 11 Sep 2023 13:23:24 -0700 Subject: [PATCH 227/237] Fixes and updates to the preferences API (#1575) * Fix to handle duplicate preference key entries * Add personal details preference API to sdk * Add Array.prototype.findLast() type declaration * Move interface declaration to ensure it's included in other package builds --- packages/api/src/bsky-agent.ts | 72 ++++++++-- packages/api/src/types.ts | 1 + packages/api/tests/bsky-agent.test.ts | 182 ++++++++++++++++++++++++++ 3 files changed, 244 insertions(+), 11 deletions(-) diff --git a/packages/api/src/bsky-agent.ts b/packages/api/src/bsky-agent.ts index 29fedfa2122..b7dd1bc1931 100644 --- a/packages/api/src/bsky-agent.ts +++ b/packages/api/src/bsky-agent.ts @@ -8,6 +8,15 @@ import { } from './client' import { BskyPreferences, BskyLabelPreference } from './types' +declare global { + interface Array { + findLast( + predicate: (value: T, index: number, obj: T[]) => unknown, + thisArg?: any, + ): T + } +} + export class BskyAgent extends AtpAgent { get app() { return this.api.app @@ -247,6 +256,7 @@ export class BskyAgent extends AtpAgent { }, adultContentEnabled: false, contentLabels: {}, + birthDate: undefined, } const res = await this.app.bsky.actor.getPreferences({}) for (const pref of res.data.preferences) { @@ -272,6 +282,13 @@ export class BskyAgent extends AtpAgent { ) { prefs.feeds.saved = pref.saved prefs.feeds.pinned = pref.pinned + } else if ( + AppBskyActorDefs.isPersonalDetailsPref(pref) && + AppBskyActorDefs.validatePersonalDetailsPref(pref).success + ) { + if (pref.birthDate) { + prefs.birthDate = new Date(pref.birthDate) + } } } return prefs @@ -314,20 +331,22 @@ export class BskyAgent extends AtpAgent { async setAdultContentEnabled(v: boolean) { await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { - const existing = prefs.find( + let adultContentPref = prefs.findLast( (pref) => AppBskyActorDefs.isAdultContentPref(pref) && AppBskyActorDefs.validateAdultContentPref(pref).success, ) - if (existing) { - existing.enabled = v + if (adultContentPref) { + adultContentPref.enabled = v } else { - prefs.push({ + adultContentPref = { $type: 'app.bsky.actor.defs#adultContentPref', enabled: v, - }) + } } return prefs + .filter((pref) => !AppBskyActorDefs.isAdultContentPref(pref)) + .concat([adultContentPref]) }) } @@ -338,22 +357,53 @@ export class BskyAgent extends AtpAgent { } await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { - const existing = prefs.find( + let labelPref = prefs.findLast( (pref) => AppBskyActorDefs.isContentLabelPref(pref) && AppBskyActorDefs.validateAdultContentPref(pref).success && pref.label === key, ) - if (existing) { - existing.visibility = value + if (labelPref) { + labelPref.visibility = value } else { - prefs.push({ + labelPref = { $type: 'app.bsky.actor.defs#contentLabelPref', label: key, visibility: value, - }) + } + } + return prefs + .filter( + (pref) => + !AppBskyActorDefs.isContentLabelPref(pref) || pref.label !== key, + ) + .concat([labelPref]) + }) + } + + async setPersonalDetails({ + birthDate, + }: { + birthDate: string | Date | undefined + }) { + birthDate = birthDate instanceof Date ? birthDate.toISOString() : birthDate + await updatePreferences(this, (prefs: AppBskyActorDefs.Preferences) => { + let personalDetailsPref = prefs.findLast( + (pref) => + AppBskyActorDefs.isPersonalDetailsPref(pref) && + AppBskyActorDefs.validatePersonalDetailsPref(pref).success, + ) + if (personalDetailsPref) { + personalDetailsPref.birthDate = birthDate + } else { + personalDetailsPref = { + $type: 'app.bsky.actor.defs#personalDetailsPref', + birthDate, + } } return prefs + .filter((pref) => !AppBskyActorDefs.isPersonalDetailsPref(pref)) + .concat([personalDetailsPref]) }) } } @@ -394,7 +444,7 @@ async function updateFeedPreferences( ): Promise<{ saved: string[]; pinned: string[] }> { let res await updatePreferences(agent, (prefs: AppBskyActorDefs.Preferences) => { - let feedsPref = prefs.find( + let feedsPref = prefs.findLast( (pref) => AppBskyActorDefs.isSavedFeedsPref(pref) && AppBskyActorDefs.validateSavedFeedsPref(pref).success, diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index e8597795979..0310d6743b8 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -89,4 +89,5 @@ export interface BskyPreferences { } adultContentEnabled: boolean contentLabels: Record + birthDate: Date | undefined } diff --git a/packages/api/tests/bsky-agent.test.ts b/packages/api/tests/bsky-agent.test.ts index 981b192c1d4..24b40153458 100644 --- a/packages/api/tests/bsky-agent.test.ts +++ b/packages/api/tests/bsky-agent.test.ts @@ -215,6 +215,7 @@ describe('agent', () => { feeds: { pinned: undefined, saved: undefined }, adultContentEnabled: false, contentLabels: {}, + birthDate: undefined, }) await agent.setAdultContentEnabled(true) @@ -222,6 +223,7 @@ describe('agent', () => { feeds: { pinned: undefined, saved: undefined }, adultContentEnabled: true, contentLabels: {}, + birthDate: undefined, }) await agent.setAdultContentEnabled(false) @@ -229,6 +231,7 @@ describe('agent', () => { feeds: { pinned: undefined, saved: undefined }, adultContentEnabled: false, contentLabels: {}, + birthDate: undefined, }) await agent.setContentLabelPref('impersonation', 'warn') @@ -238,6 +241,7 @@ describe('agent', () => { contentLabels: { impersonation: 'warn', }, + birthDate: undefined, }) await agent.setContentLabelPref('spam', 'show') // will convert to 'ignore' @@ -249,6 +253,7 @@ describe('agent', () => { impersonation: 'hide', spam: 'ignore', }, + birthDate: undefined, }) await agent.addSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -262,6 +267,7 @@ describe('agent', () => { impersonation: 'hide', spam: 'ignore', }, + birthDate: undefined, }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -275,6 +281,7 @@ describe('agent', () => { impersonation: 'hide', spam: 'ignore', }, + birthDate: undefined, }) await agent.removePinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -288,6 +295,7 @@ describe('agent', () => { impersonation: 'hide', spam: 'ignore', }, + birthDate: undefined, }) await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -301,6 +309,7 @@ describe('agent', () => { impersonation: 'hide', spam: 'ignore', }, + birthDate: undefined, }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -314,6 +323,7 @@ describe('agent', () => { impersonation: 'hide', spam: 'ignore', }, + birthDate: undefined, }) await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake2') @@ -333,6 +343,7 @@ describe('agent', () => { impersonation: 'hide', spam: 'ignore', }, + birthDate: undefined, }) await agent.removeSavedFeed('at://bob.com/app.bsky.feed.generator/fake') @@ -346,7 +357,178 @@ describe('agent', () => { impersonation: 'hide', spam: 'ignore', }, + birthDate: undefined, }) + + await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' }) + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: ['at://bob.com/app.bsky.feed.generator/fake2'], + saved: ['at://bob.com/app.bsky.feed.generator/fake2'], + }, + adultContentEnabled: false, + contentLabels: { + impersonation: 'hide', + spam: 'ignore', + }, + birthDate: new Date('2023-09-11T18:05:42.556Z'), + }) + }) + + it('resolves duplicates correctly', async () => { + const agent = new BskyAgent({ service: server.url }) + + await agent.createAccount({ + handle: 'user6.test', + email: 'user6@test.com', + password: 'password', + }) + + await agent.app.bsky.actor.putPreferences({ + preferences: [ + { + $type: 'app.bsky.actor.defs#contentLabelPref', + label: 'nsfw', + visibility: 'show', + }, + { + $type: 'app.bsky.actor.defs#contentLabelPref', + label: 'nsfw', + visibility: 'hide', + }, + { + $type: 'app.bsky.actor.defs#contentLabelPref', + label: 'nsfw', + visibility: 'show', + }, + { + $type: 'app.bsky.actor.defs#contentLabelPref', + label: 'nsfw', + visibility: 'warn', + }, + { + $type: 'app.bsky.actor.defs#adultContentPref', + enabled: true, + }, + { + $type: 'app.bsky.actor.defs#adultContentPref', + enabled: false, + }, + { + $type: 'app.bsky.actor.defs#adultContentPref', + enabled: true, + }, + { + $type: 'app.bsky.actor.defs#savedFeedsPref', + pinned: [ + 'at://bob.com/app.bsky.feed.generator/fake', + 'at://bob.com/app.bsky.feed.generator/fake2', + ], + saved: [ + 'at://bob.com/app.bsky.feed.generator/fake', + 'at://bob.com/app.bsky.feed.generator/fake2', + ], + }, + { + $type: 'app.bsky.actor.defs#savedFeedsPref', + pinned: [], + saved: [], + }, + { + $type: 'app.bsky.actor.defs#personalDetailsPref', + birthDate: '2023-09-11T18:05:42.556Z', + }, + { + $type: 'app.bsky.actor.defs#personalDetailsPref', + birthDate: '2021-09-11T18:05:42.556Z', + }, + ], + }) + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: [], + saved: [], + }, + adultContentEnabled: true, + contentLabels: { + nsfw: 'warn', + }, + birthDate: new Date('2021-09-11T18:05:42.556Z'), + }) + + await agent.setAdultContentEnabled(false) + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: [], + saved: [], + }, + adultContentEnabled: false, + contentLabels: { + nsfw: 'warn', + }, + birthDate: new Date('2021-09-11T18:05:42.556Z'), + }) + + await agent.setContentLabelPref('nsfw', 'hide') + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: [], + saved: [], + }, + adultContentEnabled: false, + contentLabels: { + nsfw: 'hide', + }, + birthDate: new Date('2021-09-11T18:05:42.556Z'), + }) + + await agent.addPinnedFeed('at://bob.com/app.bsky.feed.generator/fake') + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: ['at://bob.com/app.bsky.feed.generator/fake'], + saved: ['at://bob.com/app.bsky.feed.generator/fake'], + }, + adultContentEnabled: false, + contentLabels: { + nsfw: 'hide', + }, + birthDate: new Date('2021-09-11T18:05:42.556Z'), + }) + + await agent.setPersonalDetails({ birthDate: '2023-09-11T18:05:42.556Z' }) + await expect(agent.getPreferences()).resolves.toStrictEqual({ + feeds: { + pinned: ['at://bob.com/app.bsky.feed.generator/fake'], + saved: ['at://bob.com/app.bsky.feed.generator/fake'], + }, + adultContentEnabled: false, + contentLabels: { + nsfw: 'hide', + }, + birthDate: new Date('2023-09-11T18:05:42.556Z'), + }) + + const res = await agent.app.bsky.actor.getPreferences() + await expect(res.data.preferences).toStrictEqual([ + { + $type: 'app.bsky.actor.defs#adultContentPref', + enabled: false, + }, + { + $type: 'app.bsky.actor.defs#contentLabelPref', + label: 'nsfw', + visibility: 'hide', + }, + { + $type: 'app.bsky.actor.defs#savedFeedsPref', + pinned: ['at://bob.com/app.bsky.feed.generator/fake'], + saved: ['at://bob.com/app.bsky.feed.generator/fake'], + }, + { + $type: 'app.bsky.actor.defs#personalDetailsPref', + birthDate: '2023-09-11T18:05:42.556Z', + }, + ]) }) }) }) From 841e906101232ff906632e51612986a9e5ab266d Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Mon, 11 Sep 2023 13:24:08 -0700 Subject: [PATCH 228/237] @atproto/api@0.6.12 --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 5d20b27b01a..3f4f0be06d8 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@atproto/api", - "version": "0.6.11", + "version": "0.6.12", "main": "src/index.ts", "publishConfig": { "main": "dist/index.js", From d4ebba874e2705fcaf826eca33e167ac6b7d7ab6 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 11 Sep 2023 18:49:00 -0500 Subject: [PATCH 229/237] Increase timeline threshold (#1573) * increase threshold * branch name * fix * rm build cfg --- packages/bsky/src/api/app/bsky/feed/getTimeline.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts index 55f0a2837c3..315ef0c9eb5 100644 --- a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts +++ b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts @@ -60,7 +60,7 @@ export const getTimelineSkeleton = async ( .innerJoin('follow', 'follow.subjectDid', 'feed_item.originatorDid') .where('follow.creator', '=', viewer) .innerJoin('post', 'post.uri', 'feed_item.postUri') - .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom, 1)) + .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom, 2)) .selectAll('feed_item') .select([ 'post.replyRoot', @@ -79,7 +79,7 @@ export const getTimelineSkeleton = async ( .selectFrom('feed_item') .innerJoin('post', 'post.uri', 'feed_item.postUri') .where('feed_item.originatorDid', '=', viewer) - .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom, 1)) + .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom, 2)) .selectAll('feed_item') .select([ 'post.replyRoot', From fdf6a4642fb5aabb3e1207a67535ff2c9a08d3b8 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Mon, 11 Sep 2023 19:32:04 -0500 Subject: [PATCH 230/237] Enable appview proxy in dev-env full network (#1580) proxy runtime flags --- packages/dev-env/src/bin-network.ts | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/dev-env/src/bin-network.ts b/packages/dev-env/src/bin-network.ts index 193c7ea968a..c0fe110fabd 100644 --- a/packages/dev-env/src/bin-network.ts +++ b/packages/dev-env/src/bin-network.ts @@ -24,6 +24,7 @@ const run = async () => { }, plc: { port: 2582 }, }) + await enableProxy(network) await generateMockSetup(network) console.log( @@ -39,3 +40,39 @@ const run = async () => { } run() + +// @TODO remove once we remove proxy runtime flags +const enableProxy = async (network: TestNetwork) => { + const flags = [ + 'appview-proxy:app.bsky.feed.getAuthorFeed', + 'appview-proxy:app.bsky.graph.getFollowers', + 'appview-proxy:app.bsky.feed.getPosts', + 'appview-proxy:app.bsky.graph.getFollows', + 'appview-proxy:app.bsky.feed.getLikes', + 'appview-proxy:app.bsky.feed.getRepostedBy', + 'appview-proxy:app.bsky.feed.getPostThread', + 'appview-proxy:app.bsky.actor.getProfile', + 'appview-proxy:app.bsky.actor.getProfiles', + 'appview-proxy:app.bsky.feed.getTimeline', + 'appview-proxy:app.bsky.feed.getSuggestions', + 'appview-proxy:app.bsky.feed.getFeed', + 'appview-proxy:app.bsky.feed.getActorFeeds', + 'appview-proxy:app.bsky.feed.getActorLikes', + 'appview-proxy:app.bsky.feed.getFeedGenerator', + 'appview-proxy:app.bsky.feed.getFeedGenerators', + 'appview-proxy:app.bsky.feed.getBlocks', + 'appview-proxy:app.bsky.feed.getList', + 'appview-proxy:app.bsky.notification.listNotifications', + 'appview-proxy:app.bsky.feed.getLists', + 'appview-proxy:app.bsky.feed.getListMutes', + 'appview-proxy:com.atproto.repo.getRecord', + 'appview-proxy:com.atproto.identity.resolveHandle', + 'appview-proxy:app.bsky.notification.getUnreadCount', + 'appview-proxy:app.bsky.actor.searchActorsTypeahead', + 'appview-proxy:app.bsky.actor.searchActors', + ] + await network.pds.ctx.db.db + .insertInto('runtime_flag') + .values(flags.map((name) => ({ name, value: '10' }))) + .execute() +} From 6f419790acc7241e594ff22af2a4937a9a50c7c8 Mon Sep 17 00:00:00 2001 From: devin ivy Date: Tue, 12 Sep 2023 12:07:31 -0400 Subject: [PATCH 231/237] Feature: block lists (#1531) * lexicons for block lists * reorg blockset functionality into graph service, impl block/mute filtering * apply filterBlocksAndMutes() throughout appview except feeds * update local feeds to pass through cleanFeedSkeleton(), offload block/mute application * impl for grabbing block/mute details by did pair * refactor getActorInfos away, use actor service * experiment with moving getFeedGenerators over to a pipeline * move getPostThread over to a pipeline * move feeds over to pipelines * move suggestions and likes over to pipelines * move reposted-by, follows, followers over to pipelines, tidy author feed and post thread * remove old block/mute checks * unify post presentation logic * move profiles endpoints over to pipelines * tidy * tidy * misc fixes * unify some profile hydration/presentation in appview * profile detail, split hydration and presentation, misc fixes * unify feed hydration w/ profile hydration * unify hydration step for embeds, tidy application of labels * setup indexing of list-blocks in bsky appview * apply list-blocks, impl getListBlocks, tidy getList, tests * tidy * update pds proxy snaps * update pds proxy snaps * fix snap * make algos return feed items, save work in getFeed * misc changes, tidy * tidy * fix aturi import * hoist actors out of composeThread() * tidy * run ci on all prs * format * build * proxy graph.getListBlocks * remove unneeded index * build pds * setup noop listblock indexer on pds * remove build --------- Co-authored-by: dholms --- .github/workflows/repo.yaml | 2 +- lexicons/app/bsky/graph/defs.json | 3 +- lexicons/app/bsky/graph/getListBlocks.json | 36 ++ lexicons/app/bsky/graph/listblock.json | 19 + packages/api/src/client/index.ts | 82 +++ packages/api/src/client/lexicons.ts | 74 +++ .../src/client/types/app/bsky/graph/defs.ts | 1 + .../types/app/bsky/graph/getListBlocks.ts | 38 ++ .../client/types/app/bsky/graph/listblock.ts | 26 + .../bsky/src/api/app/bsky/actor/getProfile.ts | 107 +++- .../src/api/app/bsky/actor/getProfiles.ts | 69 ++- .../src/api/app/bsky/actor/getSuggestions.ts | 154 +++-- .../src/api/app/bsky/actor/searchActors.ts | 2 +- .../app/bsky/actor/searchActorsTypeahead.ts | 11 +- .../src/api/app/bsky/feed/getActorFeeds.ts | 7 +- .../src/api/app/bsky/feed/getActorLikes.ts | 144 +++-- .../src/api/app/bsky/feed/getAuthorFeed.ts | 232 +++++--- .../bsky/src/api/app/bsky/feed/getFeed.ts | 182 ++++-- .../src/api/app/bsky/feed/getFeedGenerator.ts | 3 +- .../api/app/bsky/feed/getFeedGenerators.ts | 73 ++- .../src/api/app/bsky/feed/getFeedSkeleton.ts | 19 +- .../bsky/src/api/app/bsky/feed/getLikes.ts | 155 +++-- .../src/api/app/bsky/feed/getPostThread.ts | 173 +++--- .../bsky/src/api/app/bsky/feed/getPosts.ts | 102 +++- .../src/api/app/bsky/feed/getRepostedBy.ts | 134 +++-- .../api/app/bsky/feed/getSuggestedFeeds.ts | 4 +- .../bsky/src/api/app/bsky/feed/getTimeline.ts | 122 ++-- .../bsky/src/api/app/bsky/graph/getBlocks.ts | 5 +- .../src/api/app/bsky/graph/getFollowers.ts | 182 ++++-- .../bsky/src/api/app/bsky/graph/getFollows.ts | 183 ++++-- .../bsky/src/api/app/bsky/graph/getList.ts | 171 +++--- .../src/api/app/bsky/graph/getListBlocks.ts | 114 ++++ .../bsky/src/api/app/bsky/graph/getLists.ts | 11 +- .../bsky/src/api/app/bsky/graph/getMutes.ts | 2 +- .../bsky/notification/listNotifications.ts | 283 +++++---- .../unspecced/getPopularFeedGenerators.ts | 3 +- .../app/bsky/unspecced/getTimelineSkeleton.ts | 14 +- packages/bsky/src/api/index.ts | 2 + packages/bsky/src/db/database-schema.ts | 2 + .../20230904T211011773Z-block-lists.ts | 24 + packages/bsky/src/db/migrations/index.ts | 1 + packages/bsky/src/db/tables/list-block.ts | 15 + packages/bsky/src/feed-gen/best-of-follows.ts | 5 - packages/bsky/src/feed-gen/bsky-team.ts | 8 +- packages/bsky/src/feed-gen/hot-classic.ts | 8 +- packages/bsky/src/feed-gen/mutuals.ts | 6 +- packages/bsky/src/feed-gen/types.ts | 17 +- packages/bsky/src/feed-gen/whats-hot.ts | 8 +- packages/bsky/src/feed-gen/with-friends.ts | 1 + packages/bsky/src/lexicon/index.ts | 12 + packages/bsky/src/lexicon/lexicons.ts | 74 +++ .../src/lexicon/types/app/bsky/graph/defs.ts | 1 + .../types/app/bsky/graph/getListBlocks.ts | 48 ++ .../lexicon/types/app/bsky/graph/listblock.ts | 26 + packages/bsky/src/pipeline.ts | 22 + packages/bsky/src/services/actor/index.ts | 2 + packages/bsky/src/services/actor/types.ts | 74 +++ packages/bsky/src/services/actor/views.ts | 436 +++++++------- packages/bsky/src/services/feed/index.ts | 420 +++---------- packages/bsky/src/services/feed/types.ts | 34 +- packages/bsky/src/services/feed/views.ts | 68 ++- packages/bsky/src/services/graph/index.ts | 325 ++++++---- packages/bsky/src/services/graph/types.ts | 9 + packages/bsky/src/services/indexing/index.ts | 7 + .../services/indexing/plugins/list-block.ts | 90 +++ packages/bsky/src/services/label/index.ts | 14 +- .../__snapshots__/block-lists.test.ts.snap | 557 ++++++++++++++++++ .../views/__snapshots__/follows.test.ts.snap | 192 +++--- .../views/__snapshots__/likes.test.ts.snap | 18 +- .../__snapshots__/mute-lists.test.ts.snap | 48 +- packages/bsky/tests/views/block-lists.test.ts | 407 +++++++++++++ .../api/app/bsky/graph/getListBlocks.ts | 19 + .../pds/src/app-view/api/app/bsky/index.ts | 2 + .../src/app-view/services/indexing/index.ts | 10 +- .../app-view/services/indexing/processor.ts | 17 + packages/pds/src/lexicon/index.ts | 12 + packages/pds/src/lexicon/lexicons.ts | 74 +++ .../src/lexicon/types/app/bsky/graph/defs.ts | 1 + .../types/app/bsky/graph/getListBlocks.ts | 48 ++ .../lexicon/types/app/bsky/graph/listblock.ts | 26 + .../proxied/__snapshots__/views.test.ts.snap | 214 ++++--- packages/pds/tests/proxied/views.test.ts | 23 + 82 files changed, 4562 insertions(+), 1807 deletions(-) create mode 100644 lexicons/app/bsky/graph/getListBlocks.json create mode 100644 lexicons/app/bsky/graph/listblock.json create mode 100644 packages/api/src/client/types/app/bsky/graph/getListBlocks.ts create mode 100644 packages/api/src/client/types/app/bsky/graph/listblock.ts create mode 100644 packages/bsky/src/api/app/bsky/graph/getListBlocks.ts create mode 100644 packages/bsky/src/db/migrations/20230904T211011773Z-block-lists.ts create mode 100644 packages/bsky/src/db/tables/list-block.ts create mode 100644 packages/bsky/src/lexicon/types/app/bsky/graph/getListBlocks.ts create mode 100644 packages/bsky/src/lexicon/types/app/bsky/graph/listblock.ts create mode 100644 packages/bsky/src/pipeline.ts create mode 100644 packages/bsky/src/services/actor/types.ts create mode 100644 packages/bsky/src/services/graph/types.ts create mode 100644 packages/bsky/src/services/indexing/plugins/list-block.ts create mode 100644 packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap create mode 100644 packages/bsky/tests/views/block-lists.test.ts create mode 100644 packages/pds/src/app-view/api/app/bsky/graph/getListBlocks.ts create mode 100644 packages/pds/src/lexicon/types/app/bsky/graph/getListBlocks.ts create mode 100644 packages/pds/src/lexicon/types/app/bsky/graph/listblock.ts diff --git a/.github/workflows/repo.yaml b/.github/workflows/repo.yaml index e387c442321..8380fff8a63 100644 --- a/.github/workflows/repo.yaml +++ b/.github/workflows/repo.yaml @@ -3,7 +3,7 @@ name: Test on: pull_request: branches: - - main + - '*' concurrency: group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}' diff --git a/lexicons/app/bsky/graph/defs.json b/lexicons/app/bsky/graph/defs.json index 4366f9df368..44cf55875b4 100644 --- a/lexicons/app/bsky/graph/defs.json +++ b/lexicons/app/bsky/graph/defs.json @@ -56,7 +56,8 @@ "listViewerState": { "type": "object", "properties": { - "muted": { "type": "boolean" } + "muted": { "type": "boolean" }, + "blocked": { "type": "string", "format": "at-uri" } } } } diff --git a/lexicons/app/bsky/graph/getListBlocks.json b/lexicons/app/bsky/graph/getListBlocks.json new file mode 100644 index 00000000000..709d77aa68b --- /dev/null +++ b/lexicons/app/bsky/graph/getListBlocks.json @@ -0,0 +1,36 @@ +{ + "lexicon": 1, + "id": "app.bsky.graph.getListBlocks", + "defs": { + "main": { + "type": "query", + "description": "Which lists is the requester's account 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": "app.bsky.graph.defs#listView" } + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/graph/listblock.json b/lexicons/app/bsky/graph/listblock.json new file mode 100644 index 00000000000..b3a839c5316 --- /dev/null +++ b/lexicons/app/bsky/graph/listblock.json @@ -0,0 +1,19 @@ +{ + "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" } + } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index f9ebf0ade63..f2921d3e5a5 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -110,10 +110,12 @@ 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 AppBskyGraphList from './types/app/bsky/graph/list' +import * as AppBskyGraphListblock from './types/app/bsky/graph/listblock' import * as AppBskyGraphListitem from './types/app/bsky/graph/listitem' import * as AppBskyGraphMuteActor from './types/app/bsky/graph/muteActor' import * as AppBskyGraphMuteActorList from './types/app/bsky/graph/muteActorList' @@ -232,10 +234,12 @@ export * as AppBskyGraphGetBlocks from './types/app/bsky/graph/getBlocks' export * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers' export * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows' export * as AppBskyGraphGetList from './types/app/bsky/graph/getList' +export * as AppBskyGraphGetListBlocks from './types/app/bsky/graph/getListBlocks' export * as AppBskyGraphGetListMutes from './types/app/bsky/graph/getListMutes' export * as AppBskyGraphGetLists from './types/app/bsky/graph/getLists' export * as AppBskyGraphGetMutes from './types/app/bsky/graph/getMutes' export * as AppBskyGraphList from './types/app/bsky/graph/list' +export * as AppBskyGraphListblock from './types/app/bsky/graph/listblock' export * as AppBskyGraphListitem from './types/app/bsky/graph/listitem' export * as AppBskyGraphMuteActor from './types/app/bsky/graph/muteActor' export * as AppBskyGraphMuteActorList from './types/app/bsky/graph/muteActorList' @@ -1631,6 +1635,7 @@ export class GraphNS { block: BlockRecord follow: FollowRecord list: ListRecord + listblock: ListblockRecord listitem: ListitemRecord constructor(service: AtpServiceClient) { @@ -1638,6 +1643,7 @@ export class GraphNS { this.block = new BlockRecord(service) this.follow = new FollowRecord(service) this.list = new ListRecord(service) + this.listblock = new ListblockRecord(service) this.listitem = new ListitemRecord(service) } @@ -1685,6 +1691,17 @@ export class GraphNS { }) } + getListBlocks( + params?: AppBskyGraphGetListBlocks.QueryParams, + opts?: AppBskyGraphGetListBlocks.CallOptions, + ): Promise { + return this._service.xrpc + .call('app.bsky.graph.getListBlocks', params, undefined, opts) + .catch((e) => { + throw AppBskyGraphGetListBlocks.toKnownErr(e) + }) + } + getListMutes( params?: AppBskyGraphGetListMutes.QueryParams, opts?: AppBskyGraphGetListMutes.CallOptions, @@ -1946,6 +1963,71 @@ export class ListRecord { } } +export class ListblockRecord { + _service: AtpServiceClient + + constructor(service: AtpServiceClient) { + this._service = service + } + + async list( + params: Omit, + ): Promise<{ + cursor?: string + records: { uri: string; value: AppBskyGraphListblock.Record }[] + }> { + const res = await this._service.xrpc.call('com.atproto.repo.listRecords', { + collection: 'app.bsky.graph.listblock', + ...params, + }) + return res.data + } + + async get( + params: Omit, + ): Promise<{ + uri: string + cid: string + value: AppBskyGraphListblock.Record + }> { + const res = await this._service.xrpc.call('com.atproto.repo.getRecord', { + collection: 'app.bsky.graph.listblock', + ...params, + }) + return res.data + } + + async create( + params: Omit< + ComAtprotoRepoCreateRecord.InputSchema, + 'collection' | 'record' + >, + record: AppBskyGraphListblock.Record, + headers?: Record, + ): Promise<{ uri: string; cid: string }> { + record.$type = 'app.bsky.graph.listblock' + const res = await this._service.xrpc.call( + 'com.atproto.repo.createRecord', + undefined, + { collection: 'app.bsky.graph.listblock', ...params, record }, + { encoding: 'application/json', headers }, + ) + return res.data + } + + async delete( + params: Omit, + headers?: Record, + ): Promise { + await this._service.xrpc.call( + 'com.atproto.repo.deleteRecord', + undefined, + { collection: 'app.bsky.graph.listblock', ...params }, + { headers }, + ) + } +} + export class ListitemRecord { _service: AtpServiceClient diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 193b9c39d37..c49b098002b 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -5728,6 +5728,10 @@ export const schemaDict = { muted: { type: 'boolean', }, + blocked: { + type: 'string', + format: 'at-uri', + }, }, }, }, @@ -5956,6 +5960,49 @@ export const schemaDict = { }, }, }, + AppBskyGraphGetListBlocks: { + lexicon: 1, + id: 'app.bsky.graph.getListBlocks', + defs: { + main: { + type: 'query', + description: "Which lists is the requester's account 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', @@ -6141,6 +6188,31 @@ export const schemaDict = { }, }, }, + 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', @@ -6798,10 +6870,12 @@ export const ids = { 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', 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', diff --git a/packages/api/src/client/types/app/bsky/graph/defs.ts b/packages/api/src/client/types/app/bsky/graph/defs.ts index 2e70bef750e..566ea2446d8 100644 --- a/packages/api/src/client/types/app/bsky/graph/defs.ts +++ b/packages/api/src/client/types/app/bsky/graph/defs.ts @@ -81,6 +81,7 @@ export const MODLIST = 'app.bsky.graph.defs#modlist' export interface ListViewerState { muted?: boolean + blocked?: string [k: string]: unknown } diff --git a/packages/api/src/client/types/app/bsky/graph/getListBlocks.ts b/packages/api/src/client/types/app/bsky/graph/getListBlocks.ts new file mode 100644 index 00000000000..052587c603e --- /dev/null +++ b/packages/api/src/client/types/app/bsky/graph/getListBlocks.ts @@ -0,0 +1,38 @@ +/** + * 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 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 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/api/src/client/types/app/bsky/graph/listblock.ts b/packages/api/src/client/types/app/bsky/graph/listblock.ts new file mode 100644 index 00000000000..770dfbb0775 --- /dev/null +++ b/packages/api/src/client/types/app/bsky/graph/listblock.ts @@ -0,0 +1,26 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { ValidationResult, BlobRef } from '@atproto/lexicon' +import { isObj, hasProp } from '../../../../util' +import { lexicons } from '../../../../lexicons' +import { CID } from 'multiformats/cid' + +export interface Record { + 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/bsky/src/api/app/bsky/actor/getProfile.ts b/packages/bsky/src/api/app/bsky/actor/getProfile.ts index aab10478a32..09699b8914b 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfile.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfile.ts @@ -1,48 +1,107 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' +import { QueryParams } from '../../../../lexicon/types/app/bsky/actor/getProfile' import { softDeleted } from '../../../../db/util' import AppContext from '../../../../context' +import { Database } from '../../../../db' +import { Actor } from '../../../../db/tables/actor' +import { + ActorService, + ProfileDetailHydrationState, +} from '../../../../services/actor' import { setRepoRev } from '../../../util' +import { createPipeline, noRules } from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { + const getProfile = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.actor.getProfile({ auth: ctx.authOptionalAccessOrRoleVerifier, handler: async ({ auth, params, res }) => { - const { actor } = params - const requester = 'did' in auth.credentials ? auth.credentials.did : null - const canViewTakendownProfile = - auth.credentials.type === 'role' && auth.credentials.triage const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) + const viewer = 'did' in auth.credentials ? auth.credentials.did : null + const canViewTakendownProfile = + auth.credentials.type === 'role' && auth.credentials.triage - const [actorRes, repoRev] = await Promise.all([ - actorService.getActor(actor, true), - actorService.getRepoRev(requester), + const [result, repoRev] = await Promise.allSettled([ + getProfile( + { ...params, viewer, canViewTakendownProfile }, + { db, actorService }, + ), + actorService.getRepoRev(viewer), ]) - setRepoRev(res, repoRev) - if (!actorRes) { - throw new InvalidRequestError('Profile not found') - } - if (!canViewTakendownProfile && softDeleted(actorRes)) { - throw new InvalidRequestError( - 'Account has been taken down', - 'AccountTakedown', - ) + if (repoRev.status === 'fulfilled') { + setRepoRev(res, repoRev.value) } - const profile = await actorService.views.profileDetailed( - actorRes, - requester, - { includeSoftDeleted: canViewTakendownProfile }, - ) - if (!profile) { - throw new InvalidRequestError('Profile not found') + if (result.status === 'rejected') { + throw result.reason } return { encoding: 'application/json', - body: profile, + body: result.value, } }, }) } + +const skeleton = async ( + params: Params, + ctx: Context, +): Promise => { + const { actorService } = ctx + const { canViewTakendownProfile } = params + const actor = await actorService.getActor(params.actor, true) + if (!actor) { + throw new InvalidRequestError('Profile not found') + } + if (!canViewTakendownProfile && softDeleted(actor)) { + throw new InvalidRequestError( + 'Account has been taken down', + 'AccountTakedown', + ) + } + return { params, actor } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const { actorService } = ctx + const { params, actor } = state + const { viewer, canViewTakendownProfile } = params + const hydration = await actorService.views.profileDetailHydration( + [actor.did], + { viewer, includeSoftDeleted: canViewTakendownProfile }, + ) + return { ...state, ...hydration } +} + +const presentation = (state: HydrationState, ctx: Context) => { + const { actorService } = ctx + const { params, actor } = state + const { viewer } = params + const profiles = actorService.views.profileDetailPresentation( + [actor.did], + state, + { viewer }, + ) + const profile = profiles[actor.did] + if (!profile) { + throw new InvalidRequestError('Profile not found') + } + return profile +} + +type Context = { + db: Database + actorService: ActorService +} + +type Params = QueryParams & { + viewer: string | null + canViewTakendownProfile: boolean +} + +type SkeletonState = { params: Params; actor: Actor } + +type HydrationState = SkeletonState & ProfileDetailHydrationState diff --git a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts index 0fd7cdf844c..f2e0eb3fd50 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts @@ -1,31 +1,78 @@ +import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' +import { QueryParams } from '../../../../lexicon/types/app/bsky/actor/getProfiles' import AppContext from '../../../../context' +import { Database } from '../../../../db' +import { + ActorService, + ProfileDetailHydrationState, +} from '../../../../services/actor' import { setRepoRev } from '../../../util' +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, handler: async ({ auth, params, res }) => { - const { actors } = params - const requester = auth.credentials.did const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) + const viewer = auth.credentials.did - const [actorsRes, repoRev] = await Promise.all([ - actorService.getActors(actors), - actorService.getRepoRev(requester), + const [result, repoRev] = await Promise.all([ + getProfile({ ...params, viewer }, { db, actorService }), + actorService.getRepoRev(viewer), ]) + setRepoRev(res, repoRev) return { encoding: 'application/json', - body: { - profiles: await actorService.views.hydrateProfilesDetailed( - actorsRes, - requester, - ), - }, + body: result, } }, }) } + +const skeleton = async ( + params: Params, + ctx: Context, +): Promise => { + const { actorService } = ctx + const actors = await actorService.getActors(params.actors) + return { params, dids: actors.map((a) => a.did) } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const { actorService } = ctx + const { params, dids } = state + const { viewer } = params + const hydration = await actorService.views.profileDetailHydration(dids, { + viewer, + }) + return { ...state, ...hydration } +} + +const presentation = (state: HydrationState, ctx: Context) => { + const { actorService } = ctx + const { params, dids } = state + const { viewer } = params + const profiles = actorService.views.profileDetailPresentation(dids, state, { + viewer, + }) + const profileViews = mapDefined(dids, (did) => profiles[did]) + return { profiles: profileViews } +} + +type Context = { + db: Database + actorService: ActorService +} + +type Params = QueryParams & { + viewer: string | null +} + +type SkeletonState = { params: Params; dids: string[] } + +type HydrationState = SkeletonState & ProfileDetailHydrationState diff --git a/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts b/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts index 6a142519965..18ab99debe2 100644 --- a/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts +++ b/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts @@ -1,66 +1,126 @@ +import { mapDefined } from '@atproto/common' import AppContext from '../../../../context' +import { Database } from '../../../../db' +import { Actor } from '../../../../db/tables/actor' import { notSoftDeletedClause } from '../../../../db/util' import { Server } from '../../../../lexicon' +import { QueryParams } from '../../../../lexicon/types/app/bsky/actor/getSuggestions' +import { createPipeline } from '../../../../pipeline' +import { ActorInfoMap, ActorService } from '../../../../services/actor' +import { BlockAndMuteState, GraphService } from '../../../../services/graph' export default function (server: Server, ctx: AppContext) { + const getSuggestions = createPipeline( + skeleton, + hydration, + noBlocksOrMutes, + presentation, + ) server.app.bsky.actor.getSuggestions({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth }) => { - const { limit, cursor } = params - const viewer = auth.credentials.did - const db = ctx.db.getReplica() const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) + const viewer = auth.credentials.did - const { ref } = db.db.dynamic - - let suggestionsQb = db.db - .selectFrom('suggested_follow') - .innerJoin('actor', 'actor.did', 'suggested_follow.did') - .innerJoin('profile_agg', 'profile_agg.did', 'actor.did') - .where(notSoftDeletedClause(ref('actor'))) - .where('suggested_follow.did', '!=', viewer ?? '') - .whereNotExists((qb) => - qb - .selectFrom('follow') - .selectAll() - .where('creator', '=', viewer ?? '') - .whereRef('subjectDid', '=', ref('actor.did')), - ) - .whereNotExists(graphService.blockQb(viewer, [ref('actor.did')])) - .selectAll() - .select('profile_agg.postsCount as postsCount') - .limit(limit) - .orderBy('suggested_follow.order', 'asc') - - if (cursor) { - const cursorRow = await db.db - .selectFrom('suggested_follow') - .where('did', '=', cursor) - .selectAll() - .executeTakeFirst() - if (cursorRow) { - suggestionsQb = suggestionsQb.where( - 'suggested_follow.order', - '>', - cursorRow.order, - ) - } - } - - const suggestionsRes = await suggestionsQb.execute() + const result = await getSuggestions( + { ...params, viewer }, + { db, actorService, graphService }, + ) return { encoding: 'application/json', - body: { - cursor: suggestionsRes.at(-1)?.did, - actors: await actorService.views.hydrateProfiles( - suggestionsRes, - viewer, - ), - }, + body: result, } }, }) } + +const skeleton = async ( + params: Params, + ctx: Context, +): Promise => { + const { db } = ctx + const { limit, cursor, viewer } = params + const { ref } = db.db.dynamic + let suggestionsQb = db.db + .selectFrom('suggested_follow') + .innerJoin('actor', 'actor.did', 'suggested_follow.did') + .innerJoin('profile_agg', 'profile_agg.did', 'actor.did') + .where(notSoftDeletedClause(ref('actor'))) + .where('suggested_follow.did', '!=', viewer ?? '') + .whereNotExists((qb) => + qb + .selectFrom('follow') + .selectAll() + .where('creator', '=', viewer ?? '') + .whereRef('subjectDid', '=', ref('actor.did')), + ) + .selectAll() + .select('profile_agg.postsCount as postsCount') + .limit(limit) + .orderBy('suggested_follow.order', 'asc') + + if (cursor) { + const cursorRow = await db.db + .selectFrom('suggested_follow') + .where('did', '=', cursor) + .selectAll() + .executeTakeFirst() + if (cursorRow) { + suggestionsQb = suggestionsQb.where( + 'suggested_follow.order', + '>', + cursorRow.order, + ) + } + } + const suggestions = await suggestionsQb.execute() + return { params, suggestions, cursor: suggestions.at(-1)?.did } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const { graphService, actorService } = ctx + const { params, suggestions } = state + const { viewer } = params + const [actors, bam] = await Promise.all([ + actorService.views.profiles(suggestions, viewer), + graphService.getBlockAndMuteState( + viewer ? suggestions.map((sug) => [viewer, sug.did]) : [], + ), + ]) + return { ...state, bam, actors } +} + +const noBlocksOrMutes = (state: HydrationState) => { + const { viewer } = state.params + if (!viewer) return state + state.suggestions = state.suggestions.filter( + (item) => + !state.bam.block([viewer, item.did]) && + !state.bam.mute([viewer, item.did]), + ) + return state +} + +const presentation = (state: HydrationState) => { + const { suggestions, actors, cursor } = state + const suggestedActors = mapDefined(suggestions, (sug) => actors[sug.did]) + return { actors: suggestedActors, cursor } +} + +type Context = { + db: Database + actorService: ActorService + graphService: GraphService +} + +type Params = QueryParams & { viewer: string | null } + +type SkeletonState = { params: Params; suggestions: Actor[]; cursor?: string } + +type HydrationState = SkeletonState & { + bam: BlockAndMuteState + actors: ActorInfoMap +} diff --git a/packages/bsky/src/api/app/bsky/actor/searchActors.ts b/packages/bsky/src/api/app/bsky/actor/searchActors.ts index 9f462608885..df5821a03f9 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActors.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActors.ts @@ -27,7 +27,7 @@ export default function (server: Server, ctx: AppContext) { const actors = await ctx.services .actor(db) - .views.hydrateProfiles(results, requester) + .views.profilesList(results, requester) const filtered = actors.filter( (actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy, ) diff --git a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts index b8533073be5..64bcd811d02 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -23,11 +23,14 @@ export default function (server: Server, ctx: AppContext) { const actors = await ctx.services .actor(db) - .views.hydrateProfilesBasic(results, requester) + .views.profilesBasic(results, requester, { omitLabels: true }) - const filtered = actors.filter( - (actor) => !actor.viewer?.blocking && !actor.viewer?.blockedBy, - ) + const SKIP = [] + const filtered = results.flatMap((res) => { + const actor = actors[res.did] + if (actor.viewer?.blocking || actor.viewer?.blockedBy) return SKIP + return actor + }) return { encoding: 'application/json', diff --git a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts index a9e02d2cd59..deb5c3a5a1b 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts @@ -34,14 +34,13 @@ export default function (server: Server, ctx: AppContext) { keyset, }) - const [feedsRes, creatorProfile] = await Promise.all([ + const [feedsRes, profiles] = await Promise.all([ feedsQb.execute(), - actorService.views.profile(creatorRes, viewer), + actorService.views.profiles([creatorRes], viewer), ]) - if (!creatorProfile) { + if (!profiles[creatorRes.did]) { throw new InvalidRequestError(`Actor not found: ${actor}`) } - const profiles = { [creatorProfile.did]: creatorProfile } const feeds = feedsRes.map((row) => { const feed = { diff --git a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts index 7c634e0a810..73b8b070262 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts @@ -1,71 +1,127 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' +import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getActorLikes' import { FeedKeyset } from '../util/feed' import { paginate } from '../../../../db/pagination' import AppContext from '../../../../context' -import { InvalidRequestError } from '@atproto/xrpc-server' import { setRepoRev } from '../../../util' +import { + FeedHydrationState, + FeedRow, + FeedService, +} from '../../../../services/feed' +import { Database } from '../../../../db' +import { ActorService } from '../../../../services/actor' +import { GraphService } from '../../../../services/graph' +import { createPipeline } from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { + const getActorLikes = createPipeline( + skeleton, + hydration, + noPostBlocks, + presentation, + ) server.app.bsky.feed.getActorLikes({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth, res }) => { - const { actor, limit, cursor } = params const viewer = auth.credentials.did const db = ctx.db.getReplica() - const { ref } = db.db.dynamic - const actorService = ctx.services.actor(db) const feedService = ctx.services.feed(db) const graphService = ctx.services.graph(db) - // maybe resolve did first - const actorRes = await actorService.getActor(actor) - if (!actorRes) { - throw new InvalidRequestError('Profile not found') - } - const actorDid = actorRes.did + const [result, repoRev] = await Promise.all([ + getActorLikes( + { ...params, viewer }, + { db, actorService, feedService, graphService }, + ), + actorService.getRepoRev(viewer), + ]) + + setRepoRev(res, repoRev) - if (!viewer || viewer !== actorDid) { - throw new InvalidRequestError('Profile not found') + return { + encoding: 'application/json', + body: result, } + }, + }) +} - let feedItemsQb = feedService - .selectFeedItemQb() - .innerJoin('like', 'like.subject', 'feed_item.uri') - .where('like.creator', '=', actorDid) +const skeleton = async ( + params: Params, + ctx: Context, +): Promise => { + const { db, actorService, feedService } = ctx + const { actor, limit, cursor, viewer } = params + const { ref } = db.db.dynamic - if (viewer !== null) { - feedItemsQb = feedItemsQb.whereNotExists( - graphService.blockQb(viewer, [ref('post.creator')]), - ) - } + const actorRes = await actorService.getActor(actor) + if (!actorRes) { + throw new InvalidRequestError('Profile not found') + } + const actorDid = actorRes.did - const keyset = new FeedKeyset( - ref('feed_item.sortAt'), - ref('feed_item.cid'), - ) + if (!viewer || viewer !== actorDid) { + throw new InvalidRequestError('Profile not found') + } - feedItemsQb = paginate(feedItemsQb, { - limit, - cursor, - keyset, - }) + let feedItemsQb = feedService + .selectFeedItemQb() + .innerJoin('like', 'like.subject', 'feed_item.uri') + .where('like.creator', '=', actorDid) - const [feedItems, repoRev] = await Promise.all([ - feedItemsQb.execute(), - actorService.getRepoRev(viewer), - ]) - setRepoRev(res, repoRev) + const keyset = new FeedKeyset(ref('feed_item.sortAt'), ref('feed_item.cid')) - const feed = await feedService.hydrateFeed(feedItems, viewer) + feedItemsQb = paginate(feedItemsQb, { + limit, + cursor, + keyset, + }) - return { - encoding: 'application/json', - body: { - feed, - cursor: keyset.packFromResult(feedItems), - }, - } - }, + const feedItems = await feedItemsQb.execute() + + return { params, feedItems, cursor: keyset.packFromResult(feedItems) } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const { feedService } = ctx + const { params, feedItems } = state + const refs = feedService.feedItemRefs(feedItems) + const hydrated = await feedService.feedHydration({ + ...refs, + viewer: params.viewer, }) + return { ...state, ...hydrated } +} + +const noPostBlocks = (state: HydrationState) => { + const { viewer } = state.params + state.feedItems = state.feedItems.filter( + (item) => !viewer || !state.bam.block([viewer, item.postAuthorDid]), + ) + return state } + +const presentation = (state: HydrationState, ctx: Context) => { + const { feedService } = ctx + const { feedItems, cursor, params } = state + const feed = feedService.views.formatFeed(feedItems, state, { + viewer: params.viewer, + }) + return { feed, cursor } +} + +type Context = { + db: Database + feedService: FeedService + actorService: ActorService + graphService: GraphService +} + +type Params = QueryParams & { viewer: string | null } + +type SkeletonState = { params: Params; feedItems: FeedRow[]; cursor?: string } + +type HydrationState = SkeletonState & FeedHydrationState diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 06b93513444..9f25eb131e1 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -1,111 +1,171 @@ +import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' +import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getAuthorFeed' import { FeedKeyset } from '../util/feed' import { paginate } from '../../../../db/pagination' import AppContext from '../../../../context' -import { InvalidRequestError } from '@atproto/xrpc-server' import { setRepoRev } from '../../../util' +import { Database } from '../../../../db' +import { + FeedHydrationState, + FeedRow, + FeedService, +} from '../../../../services/feed' +import { ActorService } from '../../../../services/actor' +import { GraphService } from '../../../../services/graph' +import { createPipeline } from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { + const getAuthorFeed = createPipeline( + skeleton, + hydration, + noBlocksOrMutedReposts, + presentation, + ) server.app.bsky.feed.getAuthorFeed({ auth: ctx.authOptionalAccessOrRoleVerifier, handler: async ({ params, auth, res }) => { - const { actor, limit, cursor, filter } = params - const viewer = - auth.credentials.type === 'access' ? auth.credentials.did : null - const db = ctx.db.getReplica() - const { ref } = db.db.dynamic - - // first verify there is not a block between requester & subject - if (viewer !== null) { - const blocks = await ctx.services.graph(db).getBlocks(viewer, actor) - if (blocks.blocking) { - throw new InvalidRequestError( - `Requester has blocked actor: ${actor}`, - 'BlockedActor', - ) - } else if (blocks.blockedBy) { - throw new InvalidRequestError( - `Requester is blocked by actor: $${actor}`, - 'BlockedByActor', - ) - } - } - const 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 - // maybe resolve did first - const actorRes = await actorService.getActor(actor) - if (!actorRes) { - throw new InvalidRequestError('Profile not found') - } - const actorDid = actorRes.did - - // defaults to posts, reposts, and replies - let feedItemsQb = feedService - .selectFeedItemQb() - .where('originatorDid', '=', actorDid) - - if (filter === 'posts_with_media') { - feedItemsQb = feedItemsQb - // and only your own posts/reposts - .where('post.creator', '=', actorDid) - // only posts with media - .whereExists((qb) => - qb - .selectFrom('post_embed_image') - .select('post_embed_image.postUri') - .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'), - ) - } else if (filter === 'posts_no_replies') { - feedItemsQb = feedItemsQb.where((qb) => - qb - .where('post.replyParent', 'is', null) - .orWhere('type', '=', 'repost'), - ) - } - - if (viewer !== null) { - feedItemsQb = feedItemsQb - .where((qb) => - // Hide reposts of muted content - qb - .where('type', '=', 'post') - .orWhere((qb) => - graphService.whereNotMuted(qb, viewer, [ref('post.creator')]), - ), - ) - .whereNotExists(graphService.blockQb(viewer, [ref('post.creator')])) - } - - const keyset = new FeedKeyset( - ref('feed_item.sortAt'), - ref('feed_item.cid'), - ) - - feedItemsQb = paginate(feedItemsQb, { - limit, - cursor, - keyset, - }) - - const [feedItems, repoRev] = await Promise.all([ - feedItemsQb.execute(), + const [result, repoRev] = await Promise.all([ + getAuthorFeed( + { ...params, viewer }, + { db, actorService, feedService, graphService }, + ), actorService.getRepoRev(viewer), ]) - setRepoRev(res, repoRev) - const feed = await feedService.hydrateFeed(feedItems, viewer) + setRepoRev(res, repoRev) return { encoding: 'application/json', - body: { - feed, - cursor: keyset.packFromResult(feedItems), - }, + body: result, } }, }) } + +export const skeleton = async ( + params: Params, + ctx: Context, +): Promise => { + const { cursor, limit, actor, filter, viewer } = params + const { db, actorService, feedService, graphService } = ctx + const { ref } = db.db.dynamic + + // maybe resolve did first + const actorRes = await actorService.getActor(actor) + if (!actorRes) { + throw new InvalidRequestError('Profile not found') + } + const actorDid = actorRes.did + + // verify there is not a block between requester & subject + if (viewer !== null) { + const blocks = await graphService.getBlockState([[viewer, actorDid]]) + if (blocks.blocking([viewer, actorDid])) { + throw new InvalidRequestError( + `Requester has blocked actor: ${actor}`, + 'BlockedActor', + ) + } + if (blocks.blockedBy([viewer, actorDid])) { + throw new InvalidRequestError( + `Requester is blocked by actor: $${actor}`, + 'BlockedByActor', + ) + } + } + + // defaults to posts, reposts, and replies + let feedItemsQb = feedService + .selectFeedItemQb() + .where('originatorDid', '=', actorDid) + + if (filter === 'posts_with_media') { + feedItemsQb = feedItemsQb + // and only your own posts/reposts + .where('post.creator', '=', actorDid) + // only posts with media + .whereExists((qb) => + qb + .selectFrom('post_embed_image') + .select('post_embed_image.postUri') + .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'), + ) + } else if (filter === 'posts_no_replies') { + feedItemsQb = feedItemsQb.where((qb) => + qb.where('post.replyParent', 'is', null).orWhere('type', '=', 'repost'), + ) + } + + const keyset = new FeedKeyset(ref('feed_item.sortAt'), ref('feed_item.cid')) + + feedItemsQb = paginate(feedItemsQb, { + limit, + cursor, + keyset, + }) + + const feedItems = await feedItemsQb.execute() + + return { + params, + feedItems, + cursor: keyset.packFromResult(feedItems), + } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const { feedService } = ctx + const { params, feedItems } = state + const refs = feedService.feedItemRefs(feedItems) + const hydrated = await feedService.feedHydration({ + ...refs, + viewer: params.viewer, + }) + return { ...state, ...hydrated } +} + +const noBlocksOrMutedReposts = (state: HydrationState) => { + const { viewer } = state.params + state.feedItems = state.feedItems.filter((item) => { + if (!viewer) return true + return ( + !state.bam.block([viewer, item.postAuthorDid]) && + (item.type === 'post' || !state.bam.mute([viewer, item.postAuthorDid])) + ) + }) + return state +} + +const presentation = (state: HydrationState, ctx: Context) => { + const { feedService } = ctx + const { feedItems, cursor, params } = state + const feed = feedService.views.formatFeed(feedItems, state, { + viewer: params.viewer, + }) + return { feed, cursor } +} + +type Context = { + db: Database + actorService: ActorService + feedService: FeedService + graphService: GraphService +} + +type Params = QueryParams & { viewer: string | null } + +type SkeletonState = { + params: Params + feedItems: FeedRow[] + cursor?: string +} + +type HydrationState = SkeletonState & FeedHydrationState diff --git a/packages/bsky/src/api/app/bsky/feed/getFeed.ts b/packages/bsky/src/api/app/bsky/feed/getFeed.ts index 910ea514e94..8af159decd3 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeed.ts @@ -13,45 +13,45 @@ import { import { AtpAgent, AppBskyFeedGetFeedSkeleton } from '@atproto/api' import { QueryParams as GetFeedParams } from '../../../../lexicon/types/app/bsky/feed/getFeed' import { OutputSchema as SkeletonOutput } from '../../../../lexicon/types/app/bsky/feed/getFeedSkeleton' +import { SkeletonFeedPost } from '../../../../lexicon/types/app/bsky/feed/defs' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' import { AlgoResponse } from '../../../../feed-gen/types' import { Database } from '../../../../db' +import { + FeedHydrationState, + FeedRow, + FeedService, +} from '../../../../services/feed' +import { createPipeline } from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { + const getFeed = createPipeline( + skeleton, + hydration, + noBlocksOrMutes, + presentation, + ) server.app.bsky.feed.getFeed({ auth: ctx.authVerifierAnyAudience, handler: async ({ params, auth, req }) => { - const { feed } = params - const viewer = auth.credentials.did - const db = ctx.db.getReplica() const feedService = ctx.services.feed(db) - const localAlgo = ctx.algos[feed] - - const timerSkele = new ServerTimer('skele').start() - const { feedItems, ...rest } = - localAlgo !== undefined - ? await localAlgo(ctx, params, viewer) - : await skeletonFromFeedGen( - ctx, - db, - params, - viewer, - req.headers['authorization'], - ) - timerSkele.stop() - - const timerHydr = new ServerTimer('hydr').start() - const hydrated = await feedService.hydrateFeed(feedItems, viewer) - timerHydr.stop() + const viewer = auth.credentials.did + + const { timerSkele, timerHydr, ...result } = await getFeed( + { ...params, viewer }, + { + db, + feedService, + appCtx: ctx, + authorization: req.headers['authorization'], + }, + ) return { encoding: 'application/json', - body: { - ...rest, - feed: hydrated, - }, + body: result, headers: { 'server-timing': serverTimingHeader([timerSkele, timerHydr]), }, @@ -60,13 +60,94 @@ export default function (server: Server, ctx: AppContext) { }) } -async function skeletonFromFeedGen( - ctx: AppContext, - db: Database, +const skeleton = async ( + params: Params, + ctx: Context, +): Promise => { + const timerSkele = new ServerTimer('skele').start() + const localAlgo = ctx.appCtx.algos[params.feed] + const feedParams: GetFeedParams = { + feed: params.feed, + limit: params.limit, + cursor: params.cursor, + } + const { feedItems, cursor, ...passthrough } = + localAlgo !== undefined + ? await localAlgo(ctx.appCtx, params, params.viewer) + : await skeletonFromFeedGen(ctx, feedParams) + return { + params, + cursor, + feedItems, + timerSkele: timerSkele.stop(), + passthrough, + } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const timerHydr = new ServerTimer('hydr').start() + const { feedService } = ctx + const { params, feedItems } = state + const refs = feedService.feedItemRefs(feedItems) + const hydrated = await feedService.feedHydration({ + ...refs, + viewer: params.viewer, + }) + return { ...state, ...hydrated, timerHydr: timerHydr.stop() } +} + +const noBlocksOrMutes = (state: HydrationState) => { + const { viewer } = state.params + state.feedItems = state.feedItems.filter( + (item) => + !state.bam.block([viewer, item.postAuthorDid]) && + !state.bam.block([viewer, item.originatorDid]) && + !state.bam.mute([viewer, item.postAuthorDid]) && + !state.bam.mute([viewer, item.originatorDid]), + ) + return state +} + +const presentation = (state: HydrationState, ctx: Context) => { + const { feedService } = ctx + const { feedItems, cursor, passthrough, params } = state + const feed = feedService.views.formatFeed(feedItems, state, { + viewer: params.viewer, + }) + return { + feed, + cursor, + timerSkele: state.timerSkele, + timerHydr: state.timerHydr, + ...passthrough, + } +} + +type Context = { + db: Database + feedService: FeedService + appCtx: AppContext + authorization?: string +} + +type Params = GetFeedParams & { viewer: string } + +type SkeletonState = { + params: Params + feedItems: FeedRow[] + passthrough: Record // pass through additional items in feedgen response + cursor?: string + timerSkele: ServerTimer +} + +type HydrationState = SkeletonState & + FeedHydrationState & { feedItems: FeedRow[]; timerHydr: ServerTimer } + +const skeletonFromFeedGen = async ( + ctx: Context, params: GetFeedParams, - viewer: string, - authorization?: string, -): Promise { +): Promise => { + const { db, appCtx, authorization } = ctx const { feed } = params // Resolve and fetch feed skeleton const found = await db.db @@ -81,7 +162,7 @@ async function skeletonFromFeedGen( let resolved: DidDocument | null try { - resolved = await ctx.idResolver.did.resolve(feedDid) + resolved = await appCtx.idResolver.did.resolve(feedDid) } catch (err) { if (err instanceof PoorlyFormattedDidDocumentError) { throw new InvalidRequestError(`invalid did document: ${feedDid}`) @@ -105,7 +186,7 @@ async function skeletonFromFeedGen( try { // @TODO currently passthrough auth headers from pds const headers: Record = authorization - ? { authorization } + ? { authorization: authorization } : {} const result = await agent.api.app.bsky.feed.getFeedSkeleton(params, { headers, @@ -129,13 +210,34 @@ async function skeletonFromFeedGen( throw err } - const { feed: skeletonFeed, ...rest } = skeleton - const cleanedFeed = await ctx.services - .feed(db) - .cleanFeedSkeleton(skeletonFeed, params.limit, viewer) + const { feed: feedSkele, ...skele } = skeleton + const feedItems = await skeletonToFeedItems( + feedSkele.slice(0, params.limit), + ctx, + ) - return { - ...rest, - feedItems: cleanedFeed, + return { ...skele, feedItems } +} + +const skeletonToFeedItems = async ( + skeleton: SkeletonFeedPost[], + ctx: Context, +): Promise => { + const { feedService } = ctx + const feedItemUris = skeleton.map(getSkeleFeedItemUri) + const feedItemsRaw = await feedService.getFeedItems(feedItemUris) + const results: FeedRow[] = [] + for (const skeleItem of skeleton) { + const feedItem = feedItemsRaw[getSkeleFeedItemUri(skeleItem)] + if (feedItem && feedItem.postUri === skeleItem.post) { + results.push(feedItem) + } } + return results +} + +const getSkeleFeedItemUri = (item: SkeletonFeedPost) => { + return typeof item.reason?.repost === 'string' + ? item.reason.repost + : item.post } diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts b/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts index 2f433e3f0db..6207ba1e1aa 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts @@ -16,6 +16,7 @@ export default function (server: Server, ctx: AppContext) { const db = ctx.db.getReplica() const feedService = ctx.services.feed(db) + const actorService = ctx.services.actor(db) const got = await feedService.getFeedGeneratorInfos([feed], viewer) const feedInfo = got[feed] @@ -46,7 +47,7 @@ export default function (server: Server, ctx: AppContext) { ) } - const profiles = await feedService.getActorInfos( + const profiles = await actorService.views.profilesBasic( [feedInfo.creator], viewer, ) diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts b/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts index a81d962cb8b..a973ee6c2fb 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts @@ -1,32 +1,79 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { FeedGenInfo, FeedService } from '../../../../services/feed' +import { createPipeline, noRules } from '../../../../pipeline' +import { ActorInfoMap, ActorService } from '../../../../services/actor' +import { Database } from '../../../../db' export default function (server: Server, ctx: AppContext) { + const getFeedGenerators = createPipeline( + skeleton, + hydration, + noRules, + presentation, + ) server.app.bsky.feed.getFeedGenerators({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth }) => { const { feeds } = params - const requester = auth.credentials.did - + const viewer = auth.credentials.did const db = ctx.db.getReplica() const feedService = ctx.services.feed(db) + const actorService = ctx.services.actor(db) - const genInfos = await feedService.getFeedGeneratorInfos(feeds, requester) - const genList = Object.values(genInfos) - - const creators = genList.map((gen) => gen.creator) - const profiles = await feedService.getActorInfos(creators, requester) - - const feedViews = genList.map((gen) => - feedService.views.formatFeedGeneratorView(gen, profiles), + const view = await getFeedGenerators( + { feeds, viewer }, + { db, feedService, actorService }, ) return { encoding: 'application/json', - body: { - feeds: feedViews, - }, + body: view, } }, }) } + +const skeleton = async (params: Params, ctx: Context) => { + const { feedService } = ctx + const genInfos = await feedService.getFeedGeneratorInfos( + params.feeds, + params.viewer, + ) + return { + params, + generators: Object.values(genInfos), + } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const { actorService } = ctx + const profiles = await actorService.views.profilesBasic( + state.generators.map((gen) => gen.creator), + state.params.viewer, + ) + return { + ...state, + profiles, + } +} + +const presentation = (state: HydrationState, ctx: Context) => { + const { feedService } = ctx + const feeds = state.generators.map((gen) => + feedService.views.formatFeedGeneratorView(gen, state.profiles), + ) + return { feeds } +} + +type Context = { + db: Database + feedService: FeedService + actorService: ActorService +} + +type Params = { viewer: string | null; feeds: string[] } + +type SkeletonState = { params: Params; generators: FeedGenInfo[] } + +type HydrationState = SkeletonState & { profiles: ActorInfoMap } diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts b/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts index 9f808b35726..5d65044f86f 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedSkeleton.ts @@ -1,6 +1,7 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { toSkeletonItem } from '../../../../feed-gen/types' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeedSkeleton({ @@ -14,24 +15,14 @@ export default function (server: Server, ctx: AppContext) { throw new InvalidRequestError('Unknown feed', 'UnknownFeed') } - const { cursor, feedItems } = await localAlgo(ctx, params, viewer) - - const skeleton = feedItems.map((item) => ({ - post: item.postUri, - reason: - item.uri === item.postUri - ? undefined - : { - $type: 'app.bsky.feed.defs#skeletonReasonRepost', - repost: item.uri, - }, - })) + const result = await localAlgo(ctx, params, viewer) return { encoding: 'application/json', body: { - cursor, - feed: skeleton, + // @TODO should we proactively filter blocks/mutes from the skeleton, or treat this similar to other custom feeds? + feed: result.feedItems.map(toSkeletonItem), + cursor: result.cursor, }, } }, diff --git a/packages/bsky/src/api/app/bsky/feed/getLikes.ts b/packages/bsky/src/api/app/bsky/feed/getLikes.ts index 7ecd3b4e2db..893617f6bb0 100644 --- a/packages/bsky/src/api/app/bsky/feed/getLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getLikes.ts @@ -1,70 +1,127 @@ import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' +import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getLikes' import { paginate, TimeCidKeyset } from '../../../../db/pagination' import AppContext from '../../../../context' import { notSoftDeletedClause } from '../../../../db/util' +import { BlockAndMuteState, GraphService } from '../../../../services/graph' +import { ActorInfoMap, ActorService } from '../../../../services/actor' +import { Actor } from '../../../../db/tables/actor' +import { Database } from '../../../../db' +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, handler: async ({ params, auth }) => { - const { uri, limit, cursor, cid } = params - const requester = auth.credentials.did - const db = ctx.db.getReplica() + const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) + const viewer = auth.credentials.did - const { ref } = db.db.dynamic - - let builder = db.db - .selectFrom('like') - .where('like.subject', '=', uri) - .innerJoin('actor as creator', 'creator.did', 'like.creator') - .where(notSoftDeletedClause(ref('creator'))) - .whereNotExists(graphService.blockQb(requester, [ref('like.creator')])) - .selectAll('creator') - .select([ - 'like.cid as cid', - 'like.createdAt as createdAt', - 'like.indexedAt as indexedAt', - 'like.sortAt as sortAt', - ]) - - if (cid) { - builder = builder.where('like.subjectCid', '=', cid) - } - - const keyset = new TimeCidKeyset(ref('like.sortAt'), ref('like.cid')) - builder = paginate(builder, { - limit, - cursor, - keyset, - }) - - const likesRes = await builder.execute() - const actors = await ctx.services - .actor(db) - .views.profiles(likesRes, requester) - - const likes = mapDefined(likesRes, (row) => - actors[row.did] - ? { - createdAt: row.createdAt, - indexedAt: row.indexedAt, - actor: actors[row.did], - } - : undefined, + const result = await getLikes( + { ...params, viewer }, + { db, actorService, graphService }, ) return { encoding: 'application/json', - body: { - uri, - cid, - cursor: keyset.packFromResult(likesRes), - likes, - }, + body: result, } }, }) } + +const skeleton = async ( + params: Params, + ctx: Context, +): Promise => { + const { db } = ctx + const { uri, cid, limit, cursor } = params + const { ref } = db.db.dynamic + + let builder = db.db + .selectFrom('like') + .where('like.subject', '=', uri) + .innerJoin('actor as creator', 'creator.did', 'like.creator') + .where(notSoftDeletedClause(ref('creator'))) + .selectAll('creator') + .select([ + 'like.cid as cid', + 'like.createdAt as createdAt', + 'like.indexedAt as indexedAt', + 'like.sortAt as sortAt', + ]) + + if (cid) { + builder = builder.where('like.subjectCid', '=', cid) + } + + const keyset = new TimeCidKeyset(ref('like.sortAt'), ref('like.cid')) + builder = paginate(builder, { + limit, + cursor, + keyset, + }) + + const likes = await builder.execute() + + return { params, likes, cursor: keyset.packFromResult(likes) } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const { graphService, actorService } = ctx + const { params, likes } = state + const { viewer } = params + const [actors, bam] = await Promise.all([ + actorService.views.profiles(likes, viewer), + graphService.getBlockAndMuteState( + viewer ? likes.map((like) => [viewer, like.did]) : [], + ), + ]) + return { ...state, bam, actors } +} + +const noBlocks = (state: HydrationState) => { + const { viewer } = state.params + if (!viewer) return state + state.likes = state.likes.filter( + (item) => !state.bam.block([viewer, item.did]), + ) + return state +} + +const presentation = (state: HydrationState) => { + const { params, likes, actors, cursor } = state + const { uri, cid } = params + const likesView = mapDefined(likes, (like) => + actors[like.did] + ? { + createdAt: like.createdAt, + indexedAt: like.indexedAt, + actor: actors[like.did], + } + : undefined, + ) + return { likes: likesView, cursor, uri, cid } +} + +type Context = { + db: Database + actorService: ActorService + graphService: GraphService +} + +type Params = QueryParams & { viewer: string | null } + +type SkeletonState = { + params: Params + likes: (Actor & { createdAt: string })[] + cursor?: string +} + +type HydrationState = SkeletonState & { + bam: BlockAndMuteState + actors: ActorInfoMap +} diff --git a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts index 95bc83e86c1..2d10ff98006 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts @@ -1,97 +1,106 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import AppContext from '../../../../context' -import { - FeedRow, - ActorInfoMap, - PostEmbedViews, - PostBlocksMap, -} from '../../../../services/feed/types' -import { FeedService, PostInfoMap } from '../../../../services/feed' -import { Labels } from '../../../../services/label' import { BlockedPost, NotFoundPost, ThreadViewPost, isNotFoundPost, } from '../../../../lexicon/types/app/bsky/feed/defs' +import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getPostThread' +import AppContext from '../../../../context' +import { + FeedService, + FeedRow, + FeedHydrationState, +} from '../../../../services/feed' import { getAncestorsAndSelfQb, getDescendentsQb, } from '../../../../services/util/post' import { Database } from '../../../../db' import { setRepoRev } from '../../../util' - -export type PostThread = { - post: FeedRow - parent?: PostThread | ParentNotFoundError - replies?: PostThread[] -} +import { createPipeline, noRules } from '../../../../pipeline' +import { ActorInfoMap, ActorService } from '../../../../services/actor' export default function (server: Server, ctx: AppContext) { + const getPostThread = createPipeline( + skeleton, + hydration, + noRules, // handled in presentation: 3p block-violating replies are turned to #blockedPost, viewer blocks turned to #notFoundPost. + presentation, + ) server.app.bsky.feed.getPostThread({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth, res }) => { - const { uri, depth, parentHeight } = params - const requester = auth.credentials.did - + const viewer = auth.credentials.did const db = ctx.db.getReplica('thread') - const actorService = ctx.services.actor(db) const feedService = ctx.services.feed(db) - const labelService = ctx.services.label(db) + const actorService = ctx.services.actor(db) - const [threadData, repoRev] = await Promise.all([ - getThreadData(ctx, db, uri, depth, parentHeight), - actorService.getRepoRev(requester), + const [result, repoRev] = await Promise.allSettled([ + getPostThread({ ...params, viewer }, { db, feedService, actorService }), + actorService.getRepoRev(viewer), ]) - setRepoRev(res, repoRev) - if (!threadData) { - throw new InvalidRequestError(`Post not found: ${uri}`, 'NotFound') + if (repoRev.status === 'fulfilled') { + setRepoRev(res, repoRev.value) } - const relevant = getRelevantIds(threadData) - const [actors, posts, labels] = await Promise.all([ - feedService.getActorInfos(Array.from(relevant.dids), requester, { - skipLabels: true, - }), - feedService.getPostInfos(Array.from(relevant.uris), requester), - labelService.getLabelsForSubjects([...relevant.uris, ...relevant.dids]), - ]) - const blocks = await feedService.blocksForPosts(posts) - const embeds = await feedService.embedsForPosts(posts, blocks, requester) - - const thread = composeThread( - threadData, - feedService, - posts, - actors, - embeds, - blocks, - labels, - ) - - if (isNotFoundPost(thread)) { - // @TODO technically this could be returned as a NotFoundPost based on lexicon - throw new InvalidRequestError(`Post not found: ${uri}`, 'NotFound') + if (result.status === 'rejected') { + throw result.reason } return { encoding: 'application/json', - body: { thread }, + body: result.value, } }, }) } +const skeleton = async (params: Params, ctx: Context) => { + const threadData = await getThreadData(params, ctx) + if (!threadData) { + throw new InvalidRequestError(`Post not found: ${params.uri}`, 'NotFound') + } + return { params, threadData } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const { feedService } = ctx + const { + threadData, + params: { viewer }, + } = state + const relevant = getRelevantIds(threadData) + const hydrated = await feedService.feedHydration({ ...relevant, viewer }) + return { ...state, ...hydrated } +} + +const presentation = (state: HydrationState, ctx: Context) => { + const { params, profiles } = state + const { actorService } = ctx + const actors = actorService.views.profileBasicPresentation( + Object.keys(profiles), + state, + { viewer: params.viewer }, + ) + const thread = composeThread(state.threadData, actors, state, ctx) + if (isNotFoundPost(thread)) { + // @TODO technically this could be returned as a NotFoundPost based on lexicon + throw new InvalidRequestError(`Post not found: ${params.uri}`, 'NotFound') + } + return { thread } +} + const composeThread = ( threadData: PostThread, - feedService: FeedService, - posts: PostInfoMap, actors: ActorInfoMap, - embeds: PostEmbedViews, - blocks: PostBlocksMap, - labels: Labels, + state: HydrationState, + ctx: Context, ) => { + const { feedService } = ctx + const { posts, embeds, blocks, labels } = state + const post = feedService.views.formatPostView( threadData.post.postUri, actors, @@ -134,30 +143,14 @@ const composeThread = ( notFound: true, } } else { - parent = composeThread( - threadData.parent, - feedService, - posts, - actors, - embeds, - blocks, - labels, - ) + parent = composeThread(threadData.parent, actors, state, ctx) } } let replies: (ThreadViewPost | NotFoundPost | BlockedPost)[] | undefined if (threadData.replies) { replies = threadData.replies.flatMap((reply) => { - const thread = composeThread( - reply, - feedService, - posts, - actors, - embeds, - blocks, - labels, - ) + const thread = composeThread(reply, actors, state, ctx) // e.g. don't bother including #postNotFound reply placeholders for takedowns. either way matches api contract. const skip = [] return isNotFoundPost(thread) ? skip : thread @@ -195,13 +188,12 @@ const getRelevantIds = ( } const getThreadData = async ( - ctx: AppContext, - db: Database, - uri: string, - depth: number, - parentHeight: number, + params: Params, + ctx: Context, ): Promise => { - const feedService = ctx.services.feed(db) + const { db, feedService } = ctx + const { uri, depth, parentHeight } = params + const [parents, children] = await Promise.all([ getAncestorsAndSelfQb(db.db, { uri, parentHeight }) .selectFrom('ancestor') @@ -278,3 +270,24 @@ class ParentNotFoundError extends Error { super(`Parent not found: ${uri}`) } } + +type PostThread = { + post: FeedRow + parent?: PostThread | ParentNotFoundError + replies?: PostThread[] +} + +type Context = { + db: Database + feedService: FeedService + actorService: ActorService +} + +type Params = QueryParams & { viewer: string | null } + +type SkeletonState = { + params: Params + threadData: PostThread +} + +type HydrationState = SkeletonState & FeedHydrationState diff --git a/packages/bsky/src/api/app/bsky/feed/getPosts.ts b/packages/bsky/src/api/app/bsky/feed/getPosts.ts index 4b092a5e717..fc35b203034 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPosts.ts @@ -1,36 +1,96 @@ -import * as common from '@atproto/common' +import { dedupeStrs } from '@atproto/common' +import { AtUri } from '@atproto/syntax' import { Server } from '../../../../lexicon' +import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getPosts' import AppContext from '../../../../context' -import { PostView } from '../../../../lexicon/types/app/bsky/feed/defs' +import { Database } from '../../../../db' +import { FeedHydrationState, FeedService } from '../../../../services/feed' +import { createPipeline } from '../../../../pipeline' +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, handler: async ({ params, auth }) => { - const requester = auth.credentials.did - - const uris = common.dedupeStrs(params.uris) - const db = ctx.db.getReplica() - const postViews = await ctx.services - .feed(db) - .getPostViews(uris, requester) - - const posts: PostView[] = [] - for (const uri of uris) { - const post = postViews[uri] - const isBlocked = - post?.author.viewer?.blockedBy === true || - typeof post?.author.viewer?.blocking === 'string' - if (post && !isBlocked) { - posts.push(post) - } - } + const feedService = ctx.services.feed(db) + const actorService = ctx.services.actor(db) + const viewer = auth.credentials.did + + const results = await getPosts( + { ...params, viewer }, + { db, feedService, actorService }, + ) return { encoding: 'application/json', - body: { posts }, + body: results, } }, }) } + +const skeleton = async (params: Params) => { + return { params, postUris: dedupeStrs(params.uris) } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const { feedService } = ctx + const { params, postUris } = state + const uris = new Set(postUris) + const dids = new Set(postUris.map((uri) => new AtUri(uri).hostname)) + const hydrated = await feedService.feedHydration({ + uris, + dids, + viewer: params.viewer, + }) + return { ...state, ...hydrated } +} + +const noBlocks = (state: HydrationState) => { + const { viewer } = state.params + state.postUris = state.postUris.filter((uri) => { + const post = state.posts[uri] + if (!viewer || !post) return true + return !state.bam.block([viewer, post.creator]) + }) + return state +} + +const presentation = (state: HydrationState, ctx: Context) => { + const { feedService, actorService } = ctx + const { postUris, profiles, params } = state + const SKIP = [] + const actors = actorService.views.profileBasicPresentation( + Object.keys(profiles), + state, + { viewer: params.viewer }, + ) + const postViews = postUris.flatMap((uri) => { + const postView = feedService.views.formatPostView( + uri, + actors, + state.posts, + state.embeds, + state.labels, + ) + return postView ?? SKIP + }) + return { posts: postViews } +} + +type Context = { + db: Database + feedService: FeedService + actorService: ActorService +} + +type Params = QueryParams & { viewer: string | null } + +type SkeletonState = { + params: Params + postUris: string[] +} + +type HydrationState = SkeletonState & FeedHydrationState diff --git a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts index 69ad1e5c79e..5ca5c452b63 100644 --- a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts @@ -1,54 +1,118 @@ +import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' +import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getRepostedBy' import { paginate, TimeCidKeyset } from '../../../../db/pagination' import AppContext from '../../../../context' import { notSoftDeletedClause } from '../../../../db/util' +import { Database } from '../../../../db' +import { ActorInfoMap, ActorService } from '../../../../services/actor' +import { BlockAndMuteState, GraphService } from '../../../../services/graph' +import { Actor } from '../../../../db/tables/actor' +import { createPipeline } from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { + const getRepostedBy = createPipeline( + skeleton, + hydration, + noBlocks, + presentation, + ) server.app.bsky.feed.getRepostedBy({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth }) => { - const { uri, limit, cursor, cid } = params - const requester = auth.credentials.did const db = ctx.db.getReplica() + const actorService = ctx.services.actor(db) const graphService = ctx.services.graph(db) - const { ref } = db.db.dynamic - - let builder = db.db - .selectFrom('repost') - .where('repost.subject', '=', uri) - .innerJoin('actor as creator', 'creator.did', 'repost.creator') - .where(notSoftDeletedClause(ref('creator'))) - .whereNotExists( - graphService.blockQb(requester, [ref('repost.creator')]), - ) - .selectAll('creator') - .select(['repost.cid as cid', 'repost.sortAt as sortAt']) - - if (cid) { - builder = builder.where('repost.subjectCid', '=', cid) - } - - const keyset = new TimeCidKeyset(ref('repost.sortAt'), ref('repost.cid')) - builder = paginate(builder, { - limit, - cursor, - keyset, - }) + const viewer = auth.credentials.did - const repostedByRes = await builder.execute() - const repostedBy = await ctx.services - .actor(db) - .views.hydrateProfiles(repostedByRes, requester) + const result = await getRepostedBy( + { ...params, viewer }, + { db, actorService, graphService }, + ) return { encoding: 'application/json', - body: { - uri, - cid, - repostedBy, - cursor: keyset.packFromResult(repostedByRes), - }, + body: result, } }, }) } + +const skeleton = async ( + params: Params, + ctx: Context, +): Promise => { + const { db } = ctx + const { limit, cursor, uri, cid } = params + const { ref } = db.db.dynamic + + let builder = db.db + .selectFrom('repost') + .where('repost.subject', '=', uri) + .innerJoin('actor as creator', 'creator.did', 'repost.creator') + .where(notSoftDeletedClause(ref('creator'))) + .selectAll('creator') + .select(['repost.cid as cid', 'repost.sortAt as sortAt']) + + if (cid) { + builder = builder.where('repost.subjectCid', '=', cid) + } + + const keyset = new TimeCidKeyset(ref('repost.sortAt'), ref('repost.cid')) + builder = paginate(builder, { + limit, + cursor, + keyset, + }) + + const repostedBy = await builder.execute() + return { params, repostedBy, cursor: keyset.packFromResult(repostedBy) } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const { graphService, actorService } = ctx + const { params, repostedBy } = state + const { viewer } = params + const [actors, bam] = await Promise.all([ + actorService.views.profiles(repostedBy, viewer), + graphService.getBlockAndMuteState( + viewer ? repostedBy.map((item) => [viewer, item.did]) : [], + ), + ]) + return { ...state, bam, actors } +} + +const noBlocks = (state: HydrationState) => { + const { viewer } = state.params + if (!viewer) return state + state.repostedBy = state.repostedBy.filter( + (item) => !state.bam.block([viewer, item.did]), + ) + return state +} + +const presentation = (state: HydrationState) => { + const { params, repostedBy, actors, cursor } = state + const { uri, cid } = params + const repostedByView = mapDefined(repostedBy, (item) => actors[item.did]) + return { repostedBy: repostedByView, cursor, uri, cid } +} + +type Context = { + db: Database + actorService: ActorService + graphService: GraphService +} + +type Params = QueryParams & { viewer: string | null } + +type SkeletonState = { + params: Params + repostedBy: Actor[] + cursor?: string +} + +type HydrationState = SkeletonState & { + bam: BlockAndMuteState + actors: ActorInfoMap +} diff --git a/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts b/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts index 25ff4dc8f8d..1ad65cdf756 100644 --- a/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts +++ b/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts @@ -9,6 +9,7 @@ export default function (server: Server, ctx: AppContext) { const db = ctx.db.getReplica() const feedService = ctx.services.feed(db) + const actorService = ctx.services.actor(db) const feedsRes = await db.db .selectFrom('suggested_feed') .orderBy('suggested_feed.order', 'asc') @@ -20,7 +21,8 @@ export default function (server: Server, ctx: AppContext) { ) const genList = feedsRes.map((r) => genInfos[r.uri]).filter(Boolean) const creators = genList.map((gen) => gen.creator) - const profiles = await feedService.getActorInfos(creators, viewer) + const profiles = await actorService.views.profilesBasic(creators, viewer) + const feedViews = genList.map((gen) => feedService.views.formatFeedGeneratorView(gen, profiles), ) diff --git a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts index 315ef0c9eb5..9609ed6db42 100644 --- a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts +++ b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts @@ -4,54 +4,57 @@ import { FeedAlgorithm, FeedKeyset, getFeedDateThreshold } from '../util/feed' import { paginate } from '../../../../db/pagination' import AppContext from '../../../../context' import { Database } from '../../../../db' -import { SkeletonFeedPost } from '../../../../lexicon/types/app/bsky/feed/defs' +import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getTimeline' import { setRepoRev } from '../../../util' +import { + FeedHydrationState, + FeedRow, + FeedService, +} from '../../../../services/feed' +import { createPipeline } from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { + const getTimeline = createPipeline( + skeleton, + hydration, + noBlocksOrMutes, + presentation, + ) server.app.bsky.feed.getTimeline({ auth: ctx.authVerifier, handler: async ({ params, auth, res }) => { - const { algorithm, limit, cursor } = params const viewer = auth.credentials.did - - if (algorithm && algorithm !== FeedAlgorithm.ReverseChronological) { - throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`) - } const db = ctx.db.getReplica('timeline') + const feedService = ctx.services.feed(db) + const actorService = ctx.services.actor(db) - const [skeleton, repoRev] = await Promise.all([ - getTimelineSkeleton(db, viewer, limit, cursor), - ctx.services.actor(db).getRepoRev(viewer), + const [result, repoRev] = await Promise.all([ + getTimeline({ ...params, viewer }, { db, feedService }), + actorService.getRepoRev(viewer), ]) - setRepoRev(res, repoRev) - const feedService = ctx.services.feed(db) - const feedItems = await feedService.cleanFeedSkeleton( - skeleton.feed, - limit, - viewer, - ) - const feed = await feedService.hydrateFeed(feedItems, viewer) + setRepoRev(res, repoRev) return { encoding: 'application/json', - body: { - feed, - cursor: skeleton.cursor, - }, + body: result, } }, }) } -export const getTimelineSkeleton = async ( - db: Database, - viewer: string, - limit: number, - cursor?: string, -): Promise<{ feed: SkeletonFeedPost[]; cursor?: string }> => { +export const skeleton = async ( + params: Params, + ctx: Context, +): Promise => { + const { cursor, limit, algorithm, viewer } = params + const { db } = ctx const { ref } = db.db.dynamic + if (algorithm && algorithm !== FeedAlgorithm.ReverseChronological) { + throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`) + } + const keyset = new FeedKeyset(ref('feed_item.sortAt'), ref('feed_item.cid')) const sortFrom = keyset.unpack(cursor)?.primary @@ -99,26 +102,67 @@ export const getTimelineSkeleton = async ( selfQb.execute(), ]) - const feedItems = [...followRes, ...selfRes] + const feedItems: FeedRow[] = [...followRes, ...selfRes] .sort((a, b) => { if (a.sortAt > b.sortAt) return -1 if (a.sortAt < b.sortAt) return 1 return a.cid > b.cid ? -1 : 1 }) .slice(0, limit) - const feed = feedItems.map((item) => ({ - post: item.postUri, - reason: - item.uri === item.postUri - ? undefined - : { - $type: 'app.bsky.feed.defs#skeletonReasonRepost', - repost: item.uri, - }, - })) return { + params, + feedItems, cursor: keyset.packFromResult(feedItems), - feed, } } + +const hydration = async ( + state: SkeletonState, + ctx: Context, +): Promise => { + const { feedService } = ctx + const { params, feedItems } = state + const refs = feedService.feedItemRefs(feedItems) + const hydrated = await feedService.feedHydration({ + ...refs, + viewer: params.viewer, + }) + return { ...state, ...hydrated } +} + +const noBlocksOrMutes = (state: HydrationState): HydrationState => { + const { viewer } = state.params + state.feedItems = state.feedItems.filter( + (item) => + !state.bam.block([viewer, item.postAuthorDid]) && + !state.bam.block([viewer, item.originatorDid]) && + !state.bam.mute([viewer, item.postAuthorDid]) && + !state.bam.mute([viewer, item.originatorDid]), + ) + return state +} + +const presentation = (state: HydrationState, ctx: Context) => { + const { feedService } = ctx + const { feedItems, cursor, params } = state + const feed = feedService.views.formatFeed(feedItems, state, { + viewer: params.viewer, + }) + return { feed, cursor } +} + +type Context = { + db: Database + feedService: FeedService +} + +type Params = QueryParams & { viewer: string } + +type SkeletonState = { + params: Params + feedItems: FeedRow[] + cursor?: string +} + +type HydrationState = SkeletonState & FeedHydrationState diff --git a/packages/bsky/src/api/app/bsky/graph/getBlocks.ts b/packages/bsky/src/api/app/bsky/graph/getBlocks.ts index bb9c0fd2356..66b809d70ce 100644 --- a/packages/bsky/src/api/app/bsky/graph/getBlocks.ts +++ b/packages/bsky/src/api/app/bsky/graph/getBlocks.ts @@ -33,10 +33,7 @@ export default function (server: Server, ctx: AppContext) { const blocksRes = await blocksReq.execute() const actorService = ctx.services.actor(db) - const blocks = await actorService.views.hydrateProfiles( - blocksRes, - requester, - ) + const blocks = await actorService.views.profilesList(blocksRes, requester) return { encoding: 'application/json', diff --git a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts index 98f0d336551..1382c1f87c7 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts @@ -1,78 +1,146 @@ +import { mapDefined } from '@atproto/common' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' +import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getFollowers' import AppContext from '../../../../context' +import { Database } from '../../../../db' import { notSoftDeletedClause } from '../../../../db/util' +import { paginate, TimeCidKeyset } from '../../../../db/pagination' +import { Actor } from '../../../../db/tables/actor' +import { ActorInfoMap, ActorService } from '../../../../services/actor' +import { BlockAndMuteState, GraphService } from '../../../../services/graph' +import { createPipeline } from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { + const getFollowers = createPipeline( + skeleton, + hydration, + noBlocksInclInvalid, + presentation, + ) server.app.bsky.graph.getFollowers({ auth: ctx.authOptionalAccessOrRoleVerifier, handler: async ({ params, auth }) => { - const { actor, limit, cursor } = params - const requester = 'did' in auth.credentials ? auth.credentials.did : null - const canViewTakendownProfile = - auth.credentials.type === 'role' && auth.credentials.triage const db = ctx.db.getReplica() - const { ref } = db.db.dynamic - 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 subjectRes = await actorService.getActor( - actor, - canViewTakendownProfile, + const result = await getFollowers( + { ...params, viewer, canViewTakendownProfile }, + { db, actorService, graphService }, ) - if (!subjectRes) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - - let followersReq = db.db - .selectFrom('follow') - .where('follow.subjectDid', '=', subjectRes.did) - .innerJoin('actor as creator', 'creator.did', 'follow.creator') - .if(!canViewTakendownProfile, (qb) => - qb.where(notSoftDeletedClause(ref('creator'))), - ) - .whereNotExists( - graphService.blockQb(requester, [ref('follow.creator')]), - ) - .whereNotExists( - graphService.blockRefQb( - ref('follow.subjectDid'), - ref('follow.creator'), - ), - ) - .selectAll('creator') - .select(['follow.cid as cid', 'follow.sortAt as sortAt']) - - const keyset = new TimeCidKeyset(ref('follow.sortAt'), ref('follow.cid')) - followersReq = paginate(followersReq, { - limit, - cursor, - keyset, - }) - - const followersRes = await followersReq.execute() - const [followers, subject] = await Promise.all([ - actorService.views.hydrateProfiles(followersRes, requester, { - includeSoftDeleted: canViewTakendownProfile, - }), - actorService.views.profile(subjectRes, requester, { - includeSoftDeleted: canViewTakendownProfile, - }), - ]) - if (!subject) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } return { encoding: 'application/json', - body: { - subject, - followers, - cursor: keyset.packFromResult(followersRes), - }, + body: result, } }, }) } + +const skeleton = async ( + params: Params, + ctx: Context, +): Promise => { + const { db, actorService } = ctx + const { limit, cursor, actor, canViewTakendownProfile } = params + const { ref } = db.db.dynamic + + const subject = await actorService.getActor(actor, canViewTakendownProfile) + if (!subject) { + throw new InvalidRequestError(`Actor not found: ${actor}`) + } + + let followersReq = db.db + .selectFrom('follow') + .where('follow.subjectDid', '=', subject.did) + .innerJoin('actor as creator', 'creator.did', 'follow.creator') + .if(!canViewTakendownProfile, (qb) => + qb.where(notSoftDeletedClause(ref('creator'))), + ) + .selectAll('creator') + .select(['follow.cid as cid', 'follow.sortAt as sortAt']) + + const keyset = new TimeCidKeyset(ref('follow.sortAt'), ref('follow.cid')) + followersReq = paginate(followersReq, { + limit, + cursor, + keyset, + }) + + const followers = await followersReq.execute() + return { + params, + followers, + subject, + cursor: keyset.packFromResult(followers), + } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const { graphService, actorService } = ctx + const { params, followers, subject } = state + const { viewer } = params + const [actors, bam] = await Promise.all([ + actorService.views.profiles([subject, ...followers], viewer), + graphService.getBlockAndMuteState( + followers.flatMap((item) => { + if (viewer) { + return [ + [viewer, item.did], + [subject.did, item.did], + ] + } + return [[subject.did, item.did]] + }), + ), + ]) + return { ...state, bam, actors } +} + +const noBlocksInclInvalid = (state: HydrationState) => { + const { subject } = state + const { viewer } = state.params + state.followers = state.followers.filter( + (item) => + !state.bam.block([subject.did, item.did]) && + (!viewer || !state.bam.block([viewer, item.did])), + ) + return state +} + +const presentation = (state: HydrationState) => { + const { params, followers, subject, actors, cursor } = state + const subjectView = actors[subject.did] + const followersView = mapDefined(followers, (item) => actors[item.did]) + if (!subjectView) { + throw new InvalidRequestError(`Actor not found: ${params.actor}`) + } + return { followers: followersView, subject: subjectView, cursor } +} + +type Context = { + db: Database + actorService: ActorService + graphService: GraphService +} + +type Params = QueryParams & { + viewer: string | null + canViewTakendownProfile: boolean +} + +type SkeletonState = { + params: Params + followers: Actor[] + subject: Actor + cursor?: string +} + +type HydrationState = SkeletonState & { + bam: BlockAndMuteState + actors: ActorInfoMap +} diff --git a/packages/bsky/src/api/app/bsky/graph/getFollows.ts b/packages/bsky/src/api/app/bsky/graph/getFollows.ts index 344b17b0158..34b5d72a605 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollows.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollows.ts @@ -1,78 +1,147 @@ +import { mapDefined } from '@atproto/common' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' +import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getFollows' import AppContext from '../../../../context' +import { Database } from '../../../../db' import { notSoftDeletedClause } from '../../../../db/util' +import { paginate, TimeCidKeyset } from '../../../../db/pagination' +import { Actor } from '../../../../db/tables/actor' +import { ActorInfoMap, ActorService } from '../../../../services/actor' +import { BlockAndMuteState, GraphService } from '../../../../services/graph' +import { createPipeline } from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { + const getFollows = createPipeline( + skeleton, + hydration, + noBlocksInclInvalid, + presentation, + ) server.app.bsky.graph.getFollows({ auth: ctx.authOptionalAccessOrRoleVerifier, handler: async ({ params, auth }) => { - const { actor, limit, cursor } = params - const requester = 'did' in auth.credentials ? auth.credentials.did : null - const canViewTakendownProfile = - auth.credentials.type === 'role' && auth.credentials.triage const db = ctx.db.getReplica() - const { ref } = db.db.dynamic - 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 creatorRes = await actorService.getActor( - actor, - canViewTakendownProfile, + const result = await getFollows( + { ...params, viewer, canViewTakendownProfile }, + { db, actorService, graphService }, ) - if (!creatorRes) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } - - let followsReq = db.db - .selectFrom('follow') - .where('follow.creator', '=', creatorRes.did) - .innerJoin('actor as subject', 'subject.did', 'follow.subjectDid') - .if(!canViewTakendownProfile, (qb) => - qb.where(notSoftDeletedClause(ref('subject'))), - ) - .whereNotExists( - graphService.blockQb(requester, [ref('follow.subjectDid')]), - ) - .whereNotExists( - graphService.blockRefQb( - ref('follow.subjectDid'), - ref('follow.creator'), - ), - ) - .selectAll('subject') - .select(['follow.cid as cid', 'follow.sortAt as sortAt']) - - const keyset = new TimeCidKeyset(ref('follow.sortAt'), ref('follow.cid')) - followsReq = paginate(followsReq, { - limit, - cursor, - keyset, - }) - - const followsRes = await followsReq.execute() - const [follows, subject] = await Promise.all([ - actorService.views.hydrateProfiles(followsRes, requester, { - includeSoftDeleted: canViewTakendownProfile, - }), - actorService.views.profile(creatorRes, requester, { - includeSoftDeleted: canViewTakendownProfile, - }), - ]) - if (!subject) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } return { encoding: 'application/json', - body: { - subject, - follows, - cursor: keyset.packFromResult(followsRes), - }, + body: result, } }, }) } + +const skeleton = async ( + params: Params, + ctx: Context, +): Promise => { + const { db, actorService } = ctx + const { limit, cursor, actor, canViewTakendownProfile } = params + const { ref } = db.db.dynamic + + const creator = await actorService.getActor(actor, canViewTakendownProfile) + if (!creator) { + throw new InvalidRequestError(`Actor not found: ${actor}`) + } + + let followsReq = db.db + .selectFrom('follow') + .where('follow.creator', '=', creator.did) + .innerJoin('actor as subject', 'subject.did', 'follow.subjectDid') + .if(!canViewTakendownProfile, (qb) => + qb.where(notSoftDeletedClause(ref('subject'))), + ) + .selectAll('subject') + .select(['follow.cid as cid', 'follow.sortAt as sortAt']) + + const keyset = new TimeCidKeyset(ref('follow.sortAt'), ref('follow.cid')) + followsReq = paginate(followsReq, { + limit, + cursor, + keyset, + }) + + const follows = await followsReq.execute() + + return { + params, + follows, + creator, + cursor: keyset.packFromResult(follows), + } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const { graphService, actorService } = ctx + const { params, follows, creator } = state + const { viewer } = params + const [actors, bam] = await Promise.all([ + actorService.views.profiles([creator, ...follows], viewer), + graphService.getBlockAndMuteState( + follows.flatMap((item) => { + if (viewer) { + return [ + [viewer, item.did], + [creator.did, item.did], + ] + } + return [[creator.did, item.did]] + }), + ), + ]) + return { ...state, bam, actors } +} + +const noBlocksInclInvalid = (state: HydrationState) => { + const { creator } = state + const { viewer } = state.params + state.follows = state.follows.filter( + (item) => + !state.bam.block([creator.did, item.did]) && + (!viewer || !state.bam.block([viewer, item.did])), + ) + return state +} + +const presentation = (state: HydrationState) => { + const { params, follows, creator, actors, cursor } = state + const creatorView = actors[creator.did] + const followsView = mapDefined(follows, (item) => actors[item.did]) + if (!creatorView) { + throw new InvalidRequestError(`Actor not found: ${params.actor}`) + } + return { follows: followsView, subject: creatorView, cursor } +} + +type Context = { + db: Database + actorService: ActorService + graphService: GraphService +} + +type Params = QueryParams & { + viewer: string | null + canViewTakendownProfile: boolean +} + +type SkeletonState = { + params: Params + follows: Actor[] + creator: Actor + cursor?: string +} + +type HydrationState = SkeletonState & { + bam: BlockAndMuteState + actors: ActorInfoMap +} diff --git a/packages/bsky/src/api/app/bsky/graph/getList.ts b/packages/bsky/src/api/app/bsky/graph/getList.ts index 068b35fb6df..1e6775d01cb 100644 --- a/packages/bsky/src/api/app/bsky/graph/getList.ts +++ b/packages/bsky/src/api/app/bsky/graph/getList.ts @@ -1,87 +1,126 @@ +import { mapDefined } from '@atproto/common' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' +import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getList' import AppContext from '../../../../context' +import { Database } from '../../../../db' +import { paginate, TimeCidKeyset } from '../../../../db/pagination' +import { Actor } from '../../../../db/tables/actor' +import { GraphService, ListInfo } from '../../../../services/graph' +import { ActorService, ProfileHydrationState } from '../../../../services/actor' +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, handler: async ({ params, auth }) => { - const { list, limit, cursor } = params - const requester = auth.credentials.did const db = ctx.db.getReplica() - const { ref } = db.db.dynamic - const graphService = ctx.services.graph(db) + const actorService = ctx.services.actor(db) + const viewer = auth.credentials.did - const listRes = await graphService - .getListsQb(requester) - .where('list.uri', '=', list) - .executeTakeFirst() - if (!listRes) { - throw new InvalidRequestError(`List not found: ${list}`) + const result = await getList( + { ...params, viewer }, + { db, graphService, actorService }, + ) + + return { + encoding: 'application/json', + body: result, } + }, + }) +} - let itemsReq = graphService - .getListItemsQb() - .where('list_item.listUri', '=', list) - .where('list_item.creator', '=', listRes.creator) +const skeleton = async ( + params: Params, + ctx: Context, +): Promise => { + const { db, graphService } = ctx + const { list, limit, cursor, viewer } = params + const { ref } = db.db.dynamic - const keyset = new TimeCidKeyset( - ref('list_item.sortAt'), - ref('list_item.cid'), - ) - itemsReq = paginate(itemsReq, { - limit, - cursor, - keyset, - }) - const itemsRes = await itemsReq.execute() + const listRes = await graphService + .getListsQb(viewer) + .where('list.uri', '=', list) + .executeTakeFirst() + if (!listRes) { + throw new InvalidRequestError(`List not found: ${list}`) + } - const actorService = ctx.services.actor(db) - const profiles = await actorService.views.hydrateProfiles( - itemsRes, - requester, - ) + let itemsReq = graphService + .getListItemsQb() + .where('list_item.listUri', '=', list) + .where('list_item.creator', '=', listRes.creator) - const items = profiles.map((subject) => ({ subject })) + const keyset = new TimeCidKeyset( + ref('list_item.sortAt'), + ref('list_item.cid'), + ) - const creator = await actorService.views.profile(listRes, requester) - if (!creator) { - throw new InvalidRequestError(`Actor not found: ${listRes.handle}`) - } + itemsReq = paginate(itemsReq, { + limit, + cursor, + keyset, + }) - const subject = { - uri: listRes.uri, - cid: listRes.cid, - creator, - name: listRes.name, - purpose: listRes.purpose, - description: listRes.description ?? undefined, - descriptionFacets: listRes.descriptionFacets - ? JSON.parse(listRes.descriptionFacets) - : undefined, - avatar: listRes.avatarCid - ? ctx.imgUriBuilder.getPresetUri( - 'avatar', - listRes.creator, - listRes.avatarCid, - ) - : undefined, - indexedAt: listRes.indexedAt, - viewer: { - muted: !!listRes.viewerMuted, - }, - } + const listItems = await itemsReq.execute() - return { - encoding: 'application/json', - body: { - items, - list: subject, - cursor: keyset.packFromResult(itemsRes), - }, - } - }, + return { + params, + list: listRes, + listItems, + cursor: keyset.packFromResult(listItems), + } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const { actorService } = ctx + const { params, list, listItems } = state + const profileState = await actorService.views.profileHydration( + [list, ...listItems].map((x) => x.did), + { viewer: params.viewer }, + ) + return { ...state, ...profileState } +} + +const presentation = (state: HydrationState, ctx: Context) => { + const { actorService, graphService } = ctx + const { params, list, listItems, cursor, ...profileState } = state + const actors = actorService.views.profilePresentation( + Object.keys(profileState.profiles), + profileState, + { viewer: params.viewer }, + ) + const creator = actors[list.creator] + if (!creator) { + throw new InvalidRequestError(`Actor not found: ${list.handle}`) + } + const listView = graphService.formatListView(list, actors) + const items = mapDefined(listItems, (item) => { + const subject = actors[item.did] + if (!subject) return + return { subject } }) + return { list: listView, items, cursor } +} + +type Context = { + db: Database + actorService: ActorService + graphService: GraphService +} + +type Params = QueryParams & { + viewer: string | null +} + +type SkeletonState = { + params: Params + list: Actor & ListInfo + listItems: (Actor & { cid: string; sortAt: string })[] + cursor?: string } + +type HydrationState = SkeletonState & ProfileHydrationState diff --git a/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts b/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts new file mode 100644 index 00000000000..0884005b244 --- /dev/null +++ b/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts @@ -0,0 +1,114 @@ +import { Server } from '../../../../lexicon' +import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getListBlocks' +import { paginate, TimeCidKeyset } from '../../../../db/pagination' +import AppContext from '../../../../context' +import { Database } from '../../../../db' +import { Actor } from '../../../../db/tables/actor' +import { GraphService, ListInfo } from '../../../../services/graph' +import { ActorService, ProfileHydrationState } from '../../../../services/actor' +import { createPipeline, noRules } from '../../../../pipeline' + +export default function (server: Server, ctx: AppContext) { + const getListBlocks = createPipeline( + skeleton, + hydration, + noRules, + presentation, + ) + server.app.bsky.graph.getListBlocks({ + auth: ctx.authVerifier, + 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 result = await getListBlocks( + { ...params, viewer }, + { db, actorService, graphService }, + ) + + return { + encoding: 'application/json', + body: result, + } + }, + }) +} + +const skeleton = async ( + params: Params, + ctx: Context, +): Promise => { + const { db, graphService } = ctx + const { limit, cursor, viewer } = params + const { ref } = db.db.dynamic + + let listsReq = graphService + .getListsQb(viewer) + .whereExists( + db.db + .selectFrom('list_block') + .where('list_block.creator', '=', viewer) + .whereRef('list_block.subjectUri', '=', ref('list.uri')) + .selectAll(), + ) + + const keyset = new TimeCidKeyset(ref('list.createdAt'), ref('list.cid')) + + listsReq = paginate(listsReq, { + limit, + cursor, + keyset, + }) + + const listInfos = await listsReq.execute() + + return { + params, + listInfos, + cursor: keyset.packFromResult(listInfos), + } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const { actorService } = ctx + const { params, listInfos } = state + const profileState = await actorService.views.profileHydration( + listInfos.map((list) => list.creator), + { viewer: params.viewer }, + ) + return { ...state, ...profileState } +} + +const presentation = (state: HydrationState, ctx: Context) => { + const { actorService, graphService } = ctx + const { params, listInfos, cursor, ...profileState } = state + const actors = actorService.views.profilePresentation( + Object.keys(profileState.profiles), + profileState, + { viewer: params.viewer }, + ) + const lists = listInfos.map((list) => + graphService.formatListView(list, actors), + ) + return { lists, cursor } +} + +type Context = { + db: Database + actorService: ActorService + graphService: GraphService +} + +type Params = QueryParams & { + viewer: string +} + +type SkeletonState = { + params: Params + listInfos: (Actor & ListInfo)[] + cursor?: string +} + +type HydrationState = SkeletonState & ProfileHydrationState diff --git a/packages/bsky/src/api/app/bsky/graph/getLists.ts b/packages/bsky/src/api/app/bsky/graph/getLists.ts index 966bf0a594b..e6ca61fa9c7 100644 --- a/packages/bsky/src/api/app/bsky/graph/getLists.ts +++ b/packages/bsky/src/api/app/bsky/graph/getLists.ts @@ -31,19 +31,16 @@ export default function (server: Server, ctx: AppContext) { keyset, }) - const [listsRes, creator] = await Promise.all([ + const [listsRes, profiles] = await Promise.all([ listsReq.execute(), - actorService.views.profile(creatorRes, requester), + actorService.views.profiles([creatorRes], requester), ]) - if (!creator) { + if (!profiles[creatorRes.did]) { throw new InvalidRequestError(`Actor not found: ${actor}`) } - const profileMap = { - [creator.did]: creator, - } const lists = listsRes.map((row) => - graphService.formatListView(row, profileMap), + graphService.formatListView(row, profiles), ) return { diff --git a/packages/bsky/src/api/app/bsky/graph/getMutes.ts b/packages/bsky/src/api/app/bsky/graph/getMutes.ts index 0bac37edfdd..e69803d144a 100644 --- a/packages/bsky/src/api/app/bsky/graph/getMutes.ts +++ b/packages/bsky/src/api/app/bsky/graph/getMutes.ts @@ -38,7 +38,7 @@ export default function (server: Server, ctx: AppContext) { encoding: 'application/json', body: { cursor: keyset.packFromResult(mutesRes), - mutes: await actorService.views.hydrateProfiles(mutesRes, requester), + mutes: await actorService.views.profilesList(mutesRes, requester), }, } }, diff --git a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts index 1f36207820e..7bcc88f12d3 100644 --- a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts @@ -2,135 +2,198 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { jsonStringToLex } from '@atproto/lexicon' import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' +import { QueryParams } from '../../../../lexicon/types/app/bsky/notification/listNotifications' import AppContext from '../../../../context' +import { Database } from '../../../../db' import { notSoftDeletedClause } from '../../../../db/util' -import { getSelfLabels } from '../../../../services/label' +import { paginate, TimeCidKeyset } from '../../../../db/pagination' +import { BlockAndMuteState, GraphService } from '../../../../services/graph' +import { ActorInfoMap, ActorService } from '../../../../services/actor' +import { getSelfLabels, Labels, LabelService } from '../../../../services/label' +import { createPipeline } from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { + const listNotifications = createPipeline( + skeleton, + hydration, + noBlockOrMutes, + presentation, + ) server.app.bsky.notification.listNotifications({ auth: ctx.authVerifier, handler: async ({ params, auth }) => { - const { limit, cursor } = params - const requester = auth.credentials.did - if (params.seenAt) { - throw new InvalidRequestError('The seenAt parameter is unsupported') - } - 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 { ref } = db.db.dynamic - let notifBuilder = db.db - .selectFrom('notification as notif') - .innerJoin('record', 'record.uri', 'notif.recordUri') - .innerJoin('actor as author', 'author.did', 'notif.author') - .where(notSoftDeletedClause(ref('record'))) - .where(notSoftDeletedClause(ref('author'))) - .where('notif.did', '=', requester) - .where((qb) => - graphService.whereNotMuted(qb, requester, [ref('notif.author')]), - ) - .whereNotExists(graphService.blockQb(requester, [ref('notif.author')])) - .where((clause) => - clause - .where('reasonSubject', 'is', null) - .orWhereExists( - db.db - .selectFrom('record as subject') - .selectAll() - .whereRef('subject.uri', '=', ref('notif.reasonSubject')), - ), - ) - .select([ - 'notif.recordUri as uri', - 'notif.recordCid as cid', - 'author.did as authorDid', - 'author.handle as authorHandle', - 'author.indexedAt as authorIndexedAt', - 'author.takedownId as authorTakedownId', - 'notif.reason as reason', - 'notif.reasonSubject as reasonSubject', - 'notif.sortAt as indexedAt', - 'record.json as recordJson', - ]) - - const keyset = new NotifsKeyset( - ref('notif.sortAt'), - ref('notif.recordCid'), + const result = await listNotifications( + { ...params, viewer }, + { db, actorService, graphService, labelService }, ) - notifBuilder = paginate(notifBuilder, { - cursor, - limit, - keyset, - }) - - const actorStateQuery = db.db - .selectFrom('actor_state') - .selectAll() - .where('did', '=', requester) - - const [actorState, notifs] = await Promise.all([ - actorStateQuery.executeTakeFirst(), - notifBuilder.execute(), - ]) - - const seenAt = actorState?.lastSeenNotifs - - const actorService = ctx.services.actor(db) - const labelService = ctx.services.label(db) - const recordUris = notifs.map((notif) => notif.uri) - const [authors, labels] = await Promise.all([ - actorService.views.profiles( - notifs.map((notif) => ({ - did: notif.authorDid, - handle: notif.authorHandle, - indexedAt: notif.authorIndexedAt, - takedownId: notif.authorTakedownId, - })), - requester, - ), - labelService.getLabelsForUris(recordUris), - ]) - - const notifications = mapDefined(notifs, (notif) => { - const author = authors[notif.authorDid] - if (!author) return undefined - const record = jsonStringToLex(notif.recordJson) as Record< - string, - unknown - > - const recordLabels = labels[notif.uri] ?? [] - const recordSelfLabels = getSelfLabels({ - uri: notif.uri, - cid: notif.cid, - record, - }) - return { - uri: notif.uri, - cid: notif.cid, - author, - reason: notif.reason, - reasonSubject: notif.reasonSubject || undefined, - record, - isRead: seenAt ? notif.indexedAt <= seenAt : false, - indexedAt: notif.indexedAt, - labels: [...recordLabels, ...recordSelfLabels], - } - }) return { encoding: 'application/json', - body: { - notifications, - cursor: keyset.packFromResult(notifs), - }, + body: result, } }, }) } -type NotifRow = { indexedAt: string; cid: string } +const skeleton = async ( + params: Params, + ctx: Context, +): Promise => { + const { db } = ctx + const { limit, cursor, viewer } = params + const { ref } = db.db.dynamic + if (params.seenAt) { + throw new InvalidRequestError('The seenAt parameter is unsupported') + } + let notifBuilder = db.db + .selectFrom('notification as notif') + .innerJoin('record', 'record.uri', 'notif.recordUri') + .innerJoin('actor as author', 'author.did', 'notif.author') + .where(notSoftDeletedClause(ref('record'))) + .where(notSoftDeletedClause(ref('author'))) + .where('notif.did', '=', viewer) + .where((clause) => + clause + .where('reasonSubject', 'is', null) + .orWhereExists( + db.db + .selectFrom('record as subject') + .selectAll() + .whereRef('subject.uri', '=', ref('notif.reasonSubject')), + ), + ) + .select([ + 'notif.recordUri as uri', + 'notif.recordCid as cid', + 'author.did as authorDid', + 'author.handle as authorHandle', + 'author.indexedAt as authorIndexedAt', + 'author.takedownId as authorTakedownId', + 'notif.reason as reason', + 'notif.reasonSubject as reasonSubject', + 'notif.sortAt as indexedAt', + 'record.json as recordJson', + ]) + + const keyset = new NotifsKeyset(ref('notif.sortAt'), ref('notif.recordCid')) + notifBuilder = paginate(notifBuilder, { + cursor, + limit, + keyset, + }) + + const actorStateQuery = db.db + .selectFrom('actor_state') + .selectAll() + .where('did', '=', viewer) + + const [notifs, actorState] = await Promise.all([ + notifBuilder.execute(), + actorStateQuery.executeTakeFirst(), + ]) + + return { + params, + notifs, + cursor: keyset.packFromResult(notifs), + lastSeenNotifs: actorState?.lastSeenNotifs, + } +} + +const hydration = async (state: SkeletonState, ctx: Context) => { + const { graphService, actorService, labelService } = ctx + const { params, notifs } = state + const { viewer } = params + const dids = notifs.map((notif) => notif.authorDid) + const uris = notifs.map((notif) => notif.uri) + const [actors, labels, bam] = await Promise.all([ + actorService.views.profiles(dids, viewer), + labelService.getLabelsForUris(uris), + graphService.getBlockAndMuteState(dids.map((did) => [viewer, did])), + ]) + return { ...state, actors, labels, bam } +} + +const noBlockOrMutes = (state: HydrationState) => { + const { viewer } = state.params + state.notifs = state.notifs.filter( + (item) => + !state.bam.block([viewer, item.authorDid]) && + !state.bam.mute([viewer, item.authorDid]), + ) + return state +} + +const presentation = (state: HydrationState) => { + const { notifs, cursor, actors, labels, lastSeenNotifs } = state + const notifications = mapDefined(notifs, (notif) => { + const author = actors[notif.authorDid] + if (!author) return undefined + const record = jsonStringToLex(notif.recordJson) as Record + const recordLabels = labels[notif.uri] ?? [] + const recordSelfLabels = getSelfLabels({ + uri: notif.uri, + cid: notif.cid, + record, + }) + return { + uri: notif.uri, + cid: notif.cid, + author, + reason: notif.reason, + reasonSubject: notif.reasonSubject || undefined, + record, + isRead: lastSeenNotifs ? notif.indexedAt <= lastSeenNotifs : false, + indexedAt: notif.indexedAt, + labels: [...recordLabels, ...recordSelfLabels], + } + }) + return { notifications, cursor } +} + +type Context = { + db: Database + actorService: ActorService + graphService: GraphService + labelService: LabelService +} + +type Params = QueryParams & { + viewer: string +} + +type SkeletonState = { + params: Params + notifs: NotifRow[] + lastSeenNotifs?: string + cursor?: string +} + +type HydrationState = SkeletonState & { + bam: BlockAndMuteState + actors: ActorInfoMap + labels: Labels +} + +type NotifRow = { + indexedAt: string + cid: string + uri: string + authorDid: string + authorHandle: string | null + authorIndexedAt: string + authorTakedownId: number | null + reason: string + reasonSubject: string | null + recordJson: string +} + class NotifsKeyset extends TimeCidKeyset { labelResult(result: NotifRow) { return { primary: result.indexedAt, secondary: result.cid } diff --git a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts index 7c96522b6da..2971beba381 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts @@ -15,6 +15,7 @@ export default function (server: Server, ctx: AppContext) { const db = ctx.db.getReplica() const { ref } = db.db.dynamic const feedService = ctx.services.feed(db) + const actorService = ctx.services.actor(db) let inner = db.db .selectFrom('feed_generator') @@ -49,7 +50,7 @@ export default function (server: Server, ctx: AppContext) { ) const creators = Object.values(genInfos).map((gen) => gen.creator) - const profiles = await feedService.getActorInfos(creators, requester) + const profiles = await actorService.views.profiles(creators, requester) const genViews: GeneratorView[] = [] for (const row of res) { diff --git a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts index fa0e8626ca9..821eeda655f 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts @@ -1,21 +1,25 @@ import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { getTimelineSkeleton } from '../feed/getTimeline' +import { skeleton } from '../feed/getTimeline' +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, handler: async ({ auth, params }) => { - const { limit, cursor } = params + const db = ctx.db.getReplica('timeline') + const feedService = ctx.services.feed(db) const viewer = auth.credentials.did - const db = ctx.db.getReplica('timeline') - const skeleton = await getTimelineSkeleton(db, viewer, limit, cursor) + const result = await skeleton({ ...params, viewer }, { db, feedService }) return { encoding: 'application/json', - body: skeleton, + body: { + feed: result.feedItems.map(toSkeletonItem), + cursor: result.cursor, + }, } }, }) diff --git a/packages/bsky/src/api/index.ts b/packages/bsky/src/api/index.ts index e2b1515e412..ec64c2236bf 100644 --- a/packages/bsky/src/api/index.ts +++ b/packages/bsky/src/api/index.ts @@ -17,6 +17,7 @@ import getProfile from './app/bsky/actor/getProfile' import getProfiles from './app/bsky/actor/getProfiles' import getRepostedBy from './app/bsky/feed/getRepostedBy' import getBlocks from './app/bsky/graph/getBlocks' +import getListBlocks from './app/bsky/graph/getListBlocks' import getFollowers from './app/bsky/graph/getFollowers' import getFollows from './app/bsky/graph/getFollows' import getList from './app/bsky/graph/getList' @@ -75,6 +76,7 @@ export default function (server: Server, ctx: AppContext) { getProfiles(server, ctx) getRepostedBy(server, ctx) getBlocks(server, ctx) + getListBlocks(server, ctx) getFollowers(server, ctx) getFollows(server, ctx) getList(server, ctx) diff --git a/packages/bsky/src/db/database-schema.ts b/packages/bsky/src/db/database-schema.ts index 4c896ef5375..adb8c088207 100644 --- a/packages/bsky/src/db/database-schema.ts +++ b/packages/bsky/src/db/database-schema.ts @@ -12,6 +12,7 @@ import * as like from './tables/like' import * as list from './tables/list' import * as listItem from './tables/list-item' import * as listMute from './tables/list-mute' +import * as listBlock from './tables/list-block' import * as mute from './tables/mute' import * as actorBlock from './tables/actor-block' import * as feedGenerator from './tables/feed-generator' @@ -43,6 +44,7 @@ export type DatabaseSchemaType = duplicateRecord.PartialDB & list.PartialDB & listItem.PartialDB & listMute.PartialDB & + listBlock.PartialDB & mute.PartialDB & actorBlock.PartialDB & feedGenerator.PartialDB & diff --git a/packages/bsky/src/db/migrations/20230904T211011773Z-block-lists.ts b/packages/bsky/src/db/migrations/20230904T211011773Z-block-lists.ts new file mode 100644 index 00000000000..e61996c573f --- /dev/null +++ b/packages/bsky/src/db/migrations/20230904T211011773Z-block-lists.ts @@ -0,0 +1,24 @@ +import { Kysely, sql } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('list_block') + .addColumn('uri', 'varchar', (col) => col.primaryKey()) + .addColumn('cid', 'varchar', (col) => col.notNull()) + .addColumn('creator', 'varchar', (col) => col.notNull()) + .addColumn('subjectUri', 'varchar', (col) => col.notNull()) + .addColumn('createdAt', 'varchar', (col) => col.notNull()) + .addColumn('indexedAt', 'varchar', (col) => col.notNull()) + .addColumn('sortAt', 'varchar', (col) => + col + .generatedAlwaysAs(sql`least("createdAt", "indexedAt")`) + .stored() + .notNull(), + ) + .addUniqueConstraint('list_block_unique_subject', ['creator', 'subjectUri']) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('list_block').execute() +} diff --git a/packages/bsky/src/db/migrations/index.ts b/packages/bsky/src/db/migrations/index.ts index 61463fd5c4e..505f7c84909 100644 --- a/packages/bsky/src/db/migrations/index.ts +++ b/packages/bsky/src/db/migrations/index.ts @@ -26,3 +26,4 @@ export * as _20230808T172902639Z from './20230808T172902639Z-repo-rev' export * as _20230810T203349843Z from './20230810T203349843Z-action-duration' export * as _20230817T195936007Z from './20230817T195936007Z-native-notifications' export * as _20230830T205507322Z from './20230830T205507322Z-suggested-feeds' +export * as _20230904T211011773Z from './20230904T211011773Z-block-lists' diff --git a/packages/bsky/src/db/tables/list-block.ts b/packages/bsky/src/db/tables/list-block.ts new file mode 100644 index 00000000000..69936f4d7fd --- /dev/null +++ b/packages/bsky/src/db/tables/list-block.ts @@ -0,0 +1,15 @@ +import { GeneratedAlways } from 'kysely' + +export const tableName = 'list_block' + +export interface ListBlock { + uri: string + cid: string + creator: string + subjectUri: string + createdAt: string + indexedAt: string + sortAt: GeneratedAlways +} + +export type PartialDB = { [tableName]: ListBlock } diff --git a/packages/bsky/src/feed-gen/best-of-follows.ts b/packages/bsky/src/feed-gen/best-of-follows.ts index c154ad9aa0e..c1d4ee4d21b 100644 --- a/packages/bsky/src/feed-gen/best-of-follows.ts +++ b/packages/bsky/src/feed-gen/best-of-follows.ts @@ -12,7 +12,6 @@ const handler: AlgoHandler = async ( const { limit, cursor } = params const db = ctx.db.getReplica('feed') const feedService = ctx.services.feed(db) - const graphService = ctx.services.graph(db) const { ref } = db.db.dynamic @@ -31,10 +30,6 @@ const handler: AlgoHandler = async ( .whereRef('follow.subjectDid', '=', 'post.creator'), ), ) - .where((qb) => - graphService.whereNotMuted(qb, viewer, [ref('post.creator')]), - ) - .whereNotExists(graphService.blockQb(viewer, [ref('post.creator')])) .select('candidate.score') .select('candidate.cid') diff --git a/packages/bsky/src/feed-gen/bsky-team.ts b/packages/bsky/src/feed-gen/bsky-team.ts index 40e9cc63fe5..3592dd42e26 100644 --- a/packages/bsky/src/feed-gen/bsky-team.ts +++ b/packages/bsky/src/feed-gen/bsky-team.ts @@ -14,22 +14,17 @@ const BSKY_TEAM: NotEmptyArray = [ const handler: AlgoHandler = async ( ctx: AppContext, params: SkeletonParams, - viewer: string, + _viewer: string, ): Promise => { const { limit = 50, cursor } = params const db = ctx.db.getReplica('feed') const feedService = ctx.services.feed(db) - const graphService = ctx.services.graph(db) const { ref } = db.db.dynamic const postsQb = feedService .selectPostQb() .where('post.creator', 'in', BSKY_TEAM) - .where((qb) => - graphService.whereNotMuted(qb, viewer, [ref('post.creator')]), - ) - .whereNotExists(graphService.blockQb(viewer, [ref('post.creator')])) const keyset = new FeedKeyset(ref('sortAt'), ref('cid')) @@ -37,6 +32,7 @@ const handler: AlgoHandler = async ( feedQb = paginate(feedQb, { limit, cursor, keyset }) const feedItems = await feedQb.execute() + return { feedItems, cursor: keyset.packFromResult(feedItems), diff --git a/packages/bsky/src/feed-gen/hot-classic.ts b/packages/bsky/src/feed-gen/hot-classic.ts index fb191328002..c042cea7116 100644 --- a/packages/bsky/src/feed-gen/hot-classic.ts +++ b/packages/bsky/src/feed-gen/hot-classic.ts @@ -11,12 +11,11 @@ const NO_WHATS_HOT_LABELS: NotEmptyArray = ['!no-promote'] const handler: AlgoHandler = async ( ctx: AppContext, params: SkeletonParams, - viewer: string, + _viewer: string, ): Promise => { const { limit = 50, cursor } = params const db = ctx.db.getReplica('feed') const feedService = ctx.services.feed(db) - const graphService = ctx.services.graph(db) const { ref } = db.db.dynamic @@ -39,10 +38,6 @@ const handler: AlgoHandler = async ( .orWhereRef('label.uri', '=', ref('post_embed_record.embedUri')), ), ) - .where((qb) => - graphService.whereNotMuted(qb, viewer, [ref('post.creator')]), - ) - .whereNotExists(graphService.blockQb(viewer, [ref('post.creator')])) const keyset = new FeedKeyset(ref('sortAt'), ref('cid')) @@ -50,6 +45,7 @@ const handler: AlgoHandler = async ( feedQb = paginate(feedQb, { limit, cursor, keyset }) const feedItems = await feedQb.execute() + return { feedItems, cursor: keyset.packFromResult(feedItems), diff --git a/packages/bsky/src/feed-gen/mutuals.ts b/packages/bsky/src/feed-gen/mutuals.ts index c12ef713ada..65a3311a524 100644 --- a/packages/bsky/src/feed-gen/mutuals.ts +++ b/packages/bsky/src/feed-gen/mutuals.ts @@ -12,7 +12,6 @@ const handler: AlgoHandler = async ( const { limit = 50, cursor } = params const db = ctx.db.getReplica('feed') const feedService = ctx.services.feed(db) - const graphService = ctx.services.graph(db) const { ref } = db.db.dynamic @@ -40,14 +39,11 @@ const handler: AlgoHandler = async ( .orWhere('originatorDid', 'in', mutualsSubquery), ) .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom)) - .where((qb) => - graphService.whereNotMuted(qb, viewer, [ref('originatorDid')]), - ) - .whereNotExists(graphService.blockQb(viewer, [ref('originatorDid')])) feedQb = paginate(feedQb, { limit, cursor, keyset }) const feedItems = await feedQb.execute() + return { feedItems, cursor: keyset.packFromResult(feedItems), diff --git a/packages/bsky/src/feed-gen/types.ts b/packages/bsky/src/feed-gen/types.ts index 9670dff6018..11ebf53fb39 100644 --- a/packages/bsky/src/feed-gen/types.ts +++ b/packages/bsky/src/feed-gen/types.ts @@ -1,6 +1,7 @@ import AppContext from '../context' +import { SkeletonFeedPost } from '../lexicon/types/app/bsky/feed/defs' import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' -import { FeedRow } from '../services/feed/types' +import { FeedRow } from '../services/feed' export type AlgoResponse = { feedItems: FeedRow[] @@ -14,3 +15,17 @@ export type AlgoHandler = ( ) => Promise export type MountedAlgos = Record + +export const toSkeletonItem = (feedItem: { + uri: string + postUri: string +}): SkeletonFeedPost => ({ + post: feedItem.postUri, + reason: + feedItem.uri === feedItem.postUri + ? undefined + : { + $type: 'app.bsky.feed.defs#skeletonReasonRepost', + repost: feedItem.uri, + }, +}) diff --git a/packages/bsky/src/feed-gen/whats-hot.ts b/packages/bsky/src/feed-gen/whats-hot.ts index 1d74f72fcab..511c767804e 100644 --- a/packages/bsky/src/feed-gen/whats-hot.ts +++ b/packages/bsky/src/feed-gen/whats-hot.ts @@ -21,11 +21,10 @@ const NO_WHATS_HOT_LABELS: NotEmptyArray = [ const handler: AlgoHandler = async ( ctx: AppContext, params: SkeletonParams, - viewer: string, + _viewer: string, ): Promise => { const { limit, cursor } = params const db = ctx.db.getReplica('feed') - const graphService = ctx.services.graph(db) const { ref } = db.db.dynamic @@ -48,14 +47,9 @@ const handler: AlgoHandler = async ( .orWhereRef('label.uri', '=', ref('post_embed_record.embedUri')), ), ) - .where((qb) => - graphService.whereNotMuted(qb, viewer, [ref('post.creator')]), - ) - .whereNotExists(graphService.blockQb(viewer, [ref('post.creator')])) .select([ sql`${'post'}`.as('type'), 'post.uri as uri', - 'post.cid as cid', 'post.uri as postUri', 'post.creator as originatorDid', 'post.creator as postAuthorDid', diff --git a/packages/bsky/src/feed-gen/with-friends.ts b/packages/bsky/src/feed-gen/with-friends.ts index efd4b961e9d..98f784102a5 100644 --- a/packages/bsky/src/feed-gen/with-friends.ts +++ b/packages/bsky/src/feed-gen/with-friends.ts @@ -29,6 +29,7 @@ const handler: AlgoHandler = async ( postsQb = paginate(postsQb, { limit, cursor, keyset, tryIndex: true }) const feedItems = await postsQb.execute() + return { feedItems, cursor: keyset.packFromResult(feedItems), diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index a99d4d6e51b..df15a497c63 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -93,6 +93,7 @@ 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' @@ -1228,6 +1229,17 @@ export class GraphNS { 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, diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 193b9c39d37..c49b098002b 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -5728,6 +5728,10 @@ export const schemaDict = { muted: { type: 'boolean', }, + blocked: { + type: 'string', + format: 'at-uri', + }, }, }, }, @@ -5956,6 +5960,49 @@ export const schemaDict = { }, }, }, + AppBskyGraphGetListBlocks: { + lexicon: 1, + id: 'app.bsky.graph.getListBlocks', + defs: { + main: { + type: 'query', + description: "Which lists is the requester's account 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', @@ -6141,6 +6188,31 @@ export const schemaDict = { }, }, }, + 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', @@ -6798,10 +6870,12 @@ export const ids = { 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', 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', diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/defs.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/defs.ts index e50338d488d..63c05b5faa3 100644 --- a/packages/bsky/src/lexicon/types/app/bsky/graph/defs.ts +++ b/packages/bsky/src/lexicon/types/app/bsky/graph/defs.ts @@ -81,6 +81,7 @@ export const MODLIST = 'app.bsky.graph.defs#modlist' export interface ListViewerState { muted?: boolean + blocked?: string [k: string]: unknown } diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/getListBlocks.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/getListBlocks.ts new file mode 100644 index 00000000000..04cca70b44d --- /dev/null +++ b/packages/bsky/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/bsky/src/lexicon/types/app/bsky/graph/listblock.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/listblock.ts new file mode 100644 index 00000000000..59f2e057eb5 --- /dev/null +++ b/packages/bsky/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/bsky/src/pipeline.ts b/packages/bsky/src/pipeline.ts new file mode 100644 index 00000000000..7798519bfa2 --- /dev/null +++ b/packages/bsky/src/pipeline.ts @@ -0,0 +1,22 @@ +export function createPipeline< + Params, + SkeletonState, + HydrationState extends SkeletonState, + View, + Context, +>( + skeleton: (params: Params, ctx: Context) => Promise, + hydration: (state: SkeletonState, ctx: Context) => Promise, + rules: (state: HydrationState, ctx: Context) => HydrationState, + presentation: (state: HydrationState, ctx: Context) => View, +) { + return async (params: Params, ctx: Context) => { + const skeletonState = await skeleton(params, ctx) + const hydrationState = await hydration(skeletonState, ctx) + return presentation(rules(hydrationState, ctx), ctx) + } +} + +export function noRules(state: T) { + return state +} diff --git a/packages/bsky/src/services/actor/index.ts b/packages/bsky/src/services/actor/index.ts index 5012f27142f..54bfb714146 100644 --- a/packages/bsky/src/services/actor/index.ts +++ b/packages/bsky/src/services/actor/index.ts @@ -8,6 +8,8 @@ import { LabelCache } from '../../label-cache' import { TimeCidKeyset, paginate } from '../../db/pagination' import { SearchKeyset, getUserSearchQuery } from '../util/search' +export * from './types' + export class ActorService { constructor( public db: Database, diff --git a/packages/bsky/src/services/actor/types.ts b/packages/bsky/src/services/actor/types.ts new file mode 100644 index 00000000000..e853406e22e --- /dev/null +++ b/packages/bsky/src/services/actor/types.ts @@ -0,0 +1,74 @@ +import { ListViewBasic } from '../../lexicon/types/app/bsky/graph/defs' +import { Label } from '../../lexicon/types/com/atproto/label/defs' +import { BlockAndMuteState } from '../graph' +import { ListInfoMap } from '../graph/types' +import { Labels } from '../label' + +export type ActorInfo = { + did: string + handle: string + displayName?: string + description?: string // omitted from basic profile view + avatar?: string + indexedAt?: string // omitted from basic profile view + viewer?: { + muted?: boolean + mutedByList?: ListViewBasic + blockedBy?: boolean + blocking?: string + following?: string + followedBy?: string + } + labels?: Label[] +} +export type ActorInfoMap = { [did: string]: ActorInfo } + +export type ProfileViewMap = ActorInfoMap + +export type ProfileInfo = { + did: string + handle: string | null + profileUri: string | null + profileCid: string | null + displayName: string | null + description: string | null + avatarCid: string | null + indexedAt: string | null + profileJson: string | null + viewerFollowing: string | null + viewerFollowedBy: string | null +} + +export type ProfileInfoMap = { [did: string]: ProfileInfo } + +export type ProfileHydrationState = { + profiles: ProfileInfoMap + labels: Labels + lists: ListInfoMap + bam: BlockAndMuteState +} + +export type ProfileDetailInfo = ProfileInfo & { + bannerCid: string | null + followsCount: number | null + followersCount: number | null + postsCount: number | null +} + +export type ProfileDetailInfoMap = { [did: string]: ProfileDetailInfo } + +export type ProfileDetailHydrationState = { + profilesDetailed: ProfileDetailInfoMap + labels: Labels + lists: ListInfoMap + bam: BlockAndMuteState +} + +export const toMapByDid = ( + items: T[], +): Record => { + return items.reduce((cur, item) => { + cur[item.did] = item + return cur + }, {} as Record) +} diff --git a/packages/bsky/src/services/actor/views.ts b/packages/bsky/src/services/actor/views.ts index ff90a6be2c5..80652599f80 100644 --- a/packages/bsky/src/services/actor/views.ts +++ b/packages/bsky/src/services/actor/views.ts @@ -4,15 +4,23 @@ import { jsonStringToLex } from '@atproto/lexicon' import { ProfileViewDetailed, ProfileView, - ProfileViewBasic, } from '../../lexicon/types/app/bsky/actor/defs' import { Database } from '../../db' import { noMatch, notSoftDeletedClause } from '../../db/util' import { Actor } from '../../db/tables/actor' import { ImageUriBuilder } from '../../image/uri' -import { LabelService, getSelfLabels } from '../label' -import { GraphService } from '../graph' +import { LabelService, Labels, getSelfLabels } from '../label' +import { BlockAndMuteState, GraphService } from '../graph' import { LabelCache } from '../../label-cache' +import { + ActorInfoMap, + ProfileDetailHydrationState, + ProfileHydrationState, + ProfileInfoMap, + ProfileViewMap, + toMapByDid, +} from './types' +import { ListInfoMap } from '../graph/types' export class ActorViews { constructor( @@ -26,20 +34,65 @@ export class ActorViews { graph: GraphService.creator(this.imgUriBuilder)(this.db), } - async profilesDetailed( - results: ActorResult[], + async profiles( + results: (ActorResult | string)[], // @TODO simplify down to just string[] viewer: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise> { + opts?: { includeSoftDeleted?: boolean }, + ): Promise { if (results.length === 0) return {} + const dids = results.map((res) => (typeof res === 'string' ? res : res.did)) + const hydrated = await this.profileHydration(dids, { + viewer, + ...opts, + }) + return this.profilePresentation(dids, hydrated, { + viewer, + ...opts, + }) + } - const { ref } = this.db.db.dynamic - const { skipLabels = false, includeSoftDeleted = false } = opts ?? {} - const dids = results.map((r) => r.did) + async profilesBasic( + results: (ActorResult | string)[], + viewer: string | null, + opts?: { omitLabels?: boolean; includeSoftDeleted?: boolean }, + ): Promise { + if (results.length === 0) return {} + const dids = results.map((res) => (typeof res === 'string' ? res : res.did)) + const hydrated = await this.profileHydration(dids, { + viewer, + includeSoftDeleted: opts?.includeSoftDeleted, + }) + return this.profileBasicPresentation(dids, hydrated, { + viewer, + omitLabels: opts?.omitLabels, + }) + } + async profilesList( + results: ActorResult[], + viewer: string | null, + opts?: { includeSoftDeleted?: boolean }, + ): Promise { + const profiles = await this.profiles(results, viewer, opts) + return mapDefined(results, (result) => profiles[result.did]) + } + + async profileDetailHydration( + dids: string[], + opts: { + viewer?: string | null + includeSoftDeleted?: boolean + }, + state?: { + bam: BlockAndMuteState + labels: Labels + }, + ): Promise { + const { viewer = null, includeSoftDeleted } = opts + const { ref } = this.db.db.dynamic const profileInfosQb = this.db.db .selectFrom('actor') - .where('actor.did', 'in', dids) + .where('actor.did', 'in', dids.length ? dids : ['']) .leftJoin('profile', 'profile.creator', 'actor.did') .leftJoin('profile_agg', 'profile_agg.did', 'actor.did') .leftJoin('record', 'record.uri', 'profile.uri') @@ -66,138 +119,107 @@ export class ActorViews { .where('creator', '=', viewer ?? '') .whereRef('subjectDid', '=', ref('actor.did')) .select('uri') - .as('requesterFollowing'), + .as('viewerFollowing'), this.db.db .selectFrom('follow') .if(!viewer, (q) => q.where(noMatch)) .whereRef('creator', '=', ref('actor.did')) .where('subjectDid', '=', viewer ?? '') .select('uri') - .as('requesterFollowedBy'), - this.db.db - .selectFrom('actor_block') - .if(!viewer, (q) => q.where(noMatch)) - .where('creator', '=', viewer ?? '') - .whereRef('subjectDid', '=', ref('actor.did')) - .select('uri') - .as('requesterBlocking'), - this.db.db - .selectFrom('actor_block') - .if(!viewer, (q) => q.where(noMatch)) - .whereRef('creator', '=', ref('actor.did')) - .where('subjectDid', '=', viewer ?? '') - .select('uri') - .as('requesterBlockedBy'), - this.db.db - .selectFrom('mute') - .if(!viewer, (q) => q.where(noMatch)) - .whereRef('subjectDid', '=', ref('actor.did')) - .where('mutedByDid', '=', viewer ?? '') - .select('subjectDid') - .as('requesterMuted'), - this.db.db - .selectFrom('list_item') - .if(!viewer, (q) => q.where(noMatch)) - .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') - .where('list_mute.mutedByDid', '=', viewer ?? '') - .whereRef('list_item.subjectDid', '=', ref('actor.did')) - .select('list_item.listUri') - .limit(1) - .as('requesterMutedByList'), + .as('viewerFollowedBy'), ]) - - const [profileInfos, labels] = await Promise.all([ + const [profiles, labels, bam] = await Promise.all([ profileInfosQb.execute(), - this.services.label.getLabelsForSubjects(skipLabels ? [] : dids), + this.services.label.getLabelsForSubjects(dids, state?.labels), + this.services.graph.getBlockAndMuteState( + viewer ? dids.map((did) => [viewer, did]) : [], + state?.bam, + ), ]) + const listUris = mapDefined(profiles, ({ did }) => { + const list = viewer && bam.muteList([viewer, did]) + if (!list) return + return list + }) + const lists = await this.services.graph.getListViews(listUris, viewer) + return { profilesDetailed: toMapByDid(profiles), labels, bam, lists } + } - const listUris: string[] = profileInfos - .map((a) => a.requesterMutedByList) - .filter((list) => !!list) - const listViews = await this.services.graph.getListViews(listUris, viewer) - - return profileInfos.reduce((acc, cur) => { - const avatar = cur?.avatarCid - ? this.imgUriBuilder.getPresetUri('avatar', cur.did, cur.avatarCid) + profileDetailPresentation( + dids: string[], + state: ProfileDetailHydrationState, + opts: { + viewer?: string | null + }, + ): Record { + const { viewer } = opts + const { profilesDetailed, lists, labels, bam } = state + return dids.reduce((acc, did) => { + const prof = profilesDetailed[did] + if (!prof) return acc + const avatar = prof?.avatarCid + ? this.imgUriBuilder.getPresetUri('avatar', prof.did, prof.avatarCid) : undefined - const banner = cur?.bannerCid - ? this.imgUriBuilder.getPresetUri('banner', cur.did, cur.bannerCid) + const banner = prof?.bannerCid + ? this.imgUriBuilder.getPresetUri('banner', prof.did, prof.bannerCid) : undefined + const mutedByListUri = viewer && bam.muteList([viewer, did]) const mutedByList = - cur.requesterMutedByList && listViews[cur.requesterMutedByList] - ? this.services.graph.formatListViewBasic( - listViews[cur.requesterMutedByList], - ) + mutedByListUri && lists[mutedByListUri] + ? this.services.graph.formatListViewBasic(lists[mutedByListUri]) : undefined - const actorLabels = labels[cur.did] ?? [] + const actorLabels = labels[did] ?? [] const selfLabels = getSelfLabels({ - uri: cur.profileUri, - cid: cur.profileCid, + uri: prof.profileUri, + cid: prof.profileCid, record: - cur.profileJson !== null - ? (jsonStringToLex(cur.profileJson) as Record) + prof.profileJson !== null + ? (jsonStringToLex(prof.profileJson) as Record) : null, }) - const profile = { - did: cur.did, - handle: cur.handle ?? INVALID_HANDLE, - displayName: cur?.displayName || undefined, - description: cur?.description || undefined, + acc[did] = { + did: prof.did, + handle: prof.handle ?? INVALID_HANDLE, + displayName: prof?.displayName || undefined, + description: prof?.description || undefined, avatar, banner, - followsCount: cur?.followsCount ?? 0, - followersCount: cur?.followersCount ?? 0, - postsCount: cur?.postsCount ?? 0, - indexedAt: cur?.indexedAt || undefined, + followsCount: prof?.followsCount ?? 0, + followersCount: prof?.followersCount ?? 0, + postsCount: prof?.postsCount ?? 0, + indexedAt: prof?.indexedAt || undefined, viewer: viewer ? { - following: cur?.requesterFollowing || undefined, - followedBy: cur?.requesterFollowedBy || undefined, - muted: !!cur?.requesterMuted || !!cur.requesterMutedByList, + muted: bam.mute([viewer, did]), mutedByList, - blockedBy: !!cur.requesterBlockedBy, - blocking: cur.requesterBlocking || undefined, + blockedBy: !!bam.blockedBy([viewer, did]), + blocking: bam.blocking([viewer, did]) ?? undefined, + following: prof?.viewerFollowing || undefined, + followedBy: prof?.viewerFollowedBy || undefined, } : undefined, - labels: skipLabels ? undefined : [...actorLabels, ...selfLabels], + labels: [...actorLabels, ...selfLabels], } - acc[cur.did] = profile return acc }, {} as Record) } - async hydrateProfilesDetailed( - results: ActorResult[], - viewer: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const profiles = await this.profilesDetailed(results, viewer, opts) - return mapDefined(results, (result) => profiles[result.did]) - } - - async profileDetailed( - result: ActorResult, - viewer: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const profiles = await this.profilesDetailed([result], viewer, opts) - return profiles[result.did] ?? null - } - - async profiles( - results: ActorResult[], - viewer: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise> { - if (results.length === 0) return {} - + async profileHydration( + dids: string[], + opts: { + viewer?: string | null + includeSoftDeleted?: boolean + }, + state?: { + bam: BlockAndMuteState + labels: Labels + }, + ): Promise { + const { viewer = null, includeSoftDeleted } = opts const { ref } = this.db.db.dynamic - const { skipLabels = false, includeSoftDeleted = false } = opts ?? {} - const dids = results.map((r) => r.did) - const profileInfosQb = this.db.db .selectFrom('actor') - .where('actor.did', 'in', dids) + .where('actor.did', 'in', dids.length ? dids : ['']) .leftJoin('profile', 'profile.creator', 'actor.did') .leftJoin('record', 'record.uri', 'profile.uri') .if(!includeSoftDeleted, (qb) => @@ -219,154 +241,110 @@ export class ActorViews { .where('creator', '=', viewer ?? '') .whereRef('subjectDid', '=', ref('actor.did')) .select('uri') - .as('requesterFollowing'), + .as('viewerFollowing'), this.db.db .selectFrom('follow') .if(!viewer, (q) => q.where(noMatch)) .whereRef('creator', '=', ref('actor.did')) .where('subjectDid', '=', viewer ?? '') .select('uri') - .as('requesterFollowedBy'), - this.db.db - .selectFrom('actor_block') - .if(!viewer, (q) => q.where(noMatch)) - .where('creator', '=', viewer ?? '') - .whereRef('subjectDid', '=', ref('actor.did')) - .select('uri') - .as('requesterBlocking'), - this.db.db - .selectFrom('actor_block') - .if(!viewer, (q) => q.where(noMatch)) - .whereRef('creator', '=', ref('actor.did')) - .where('subjectDid', '=', viewer ?? '') - .select('uri') - .as('requesterBlockedBy'), - this.db.db - .selectFrom('mute') - .if(!viewer, (q) => q.where(noMatch)) - .whereRef('subjectDid', '=', ref('actor.did')) - .where('mutedByDid', '=', viewer ?? '') - .select('subjectDid') - .as('requesterMuted'), - this.db.db - .selectFrom('list_item') - .if(!viewer, (q) => q.where(noMatch)) - .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') - .where('list_mute.mutedByDid', '=', viewer ?? '') - .whereRef('list_item.subjectDid', '=', ref('actor.did')) - .select('list_item.listUri') - .limit(1) - .as('requesterMutedByList'), + .as('viewerFollowedBy'), ]) - - const [profileInfos, labels] = await Promise.all([ + const [profiles, labels, bam] = await Promise.all([ profileInfosQb.execute(), - this.services.label.getLabelsForSubjects(skipLabels ? [] : dids), + this.services.label.getLabelsForSubjects(dids, state?.labels), + this.services.graph.getBlockAndMuteState( + viewer ? dids.map((did) => [viewer, did]) : [], + state?.bam, + ), ]) + const listUris = mapDefined(profiles, ({ did }) => { + const list = viewer && bam.muteList([viewer, did]) + if (!list) return + return list + }) + const lists = await this.services.graph.getListViews(listUris, viewer) + return { profiles: toMapByDid(profiles), labels, bam, lists } + } - const listUris: string[] = profileInfos - .map((a) => a.requesterMutedByList) - .filter((list) => !!list) - const listViews = await this.services.graph.getListViews(listUris, viewer) - - return profileInfos.reduce((acc, cur) => { - const avatar = cur?.avatarCid - ? this.imgUriBuilder.getPresetUri('avatar', cur.did, cur.avatarCid) + profilePresentation( + dids: string[], + state: { + profiles: ProfileInfoMap + lists: ListInfoMap + labels: Labels + bam: BlockAndMuteState + }, + opts?: { + viewer?: string | null + }, + ): ProfileViewMap { + const { viewer } = opts ?? {} + const { profiles, lists, labels, bam } = state + return dids.reduce((acc, did) => { + const prof = profiles[did] + if (!prof) return acc + const avatar = prof?.avatarCid + ? this.imgUriBuilder.getPresetUri('avatar', prof.did, prof.avatarCid) : undefined + const mutedByListUri = viewer && bam.muteList([viewer, did]) const mutedByList = - cur.requesterMutedByList && listViews[cur.requesterMutedByList] - ? this.services.graph.formatListViewBasic( - listViews[cur.requesterMutedByList], - ) + mutedByListUri && lists[mutedByListUri] + ? this.services.graph.formatListViewBasic(lists[mutedByListUri]) : undefined - const actorLabels = labels[cur.did] ?? [] + const actorLabels = labels[did] ?? [] const selfLabels = getSelfLabels({ - uri: cur.profileUri, - cid: cur.profileCid, + uri: prof.profileUri, + cid: prof.profileCid, record: - cur.profileJson !== null - ? (jsonStringToLex(cur.profileJson) as Record) + prof.profileJson !== null + ? (jsonStringToLex(prof.profileJson) as Record) : null, }) - const profile = { - did: cur.did, - handle: cur.handle ?? INVALID_HANDLE, - displayName: cur?.displayName || undefined, - description: cur?.description || undefined, + acc[did] = { + did: prof.did, + handle: prof.handle ?? INVALID_HANDLE, + displayName: prof?.displayName || undefined, + description: prof?.description || undefined, avatar, - indexedAt: cur?.indexedAt || undefined, + indexedAt: prof?.indexedAt || undefined, viewer: viewer ? { - muted: !!cur?.requesterMuted || !!cur.requesterMutedByList, + muted: bam.mute([viewer, did]), mutedByList, - blockedBy: !!cur.requesterBlockedBy, - blocking: cur.requesterBlocking || undefined, - following: cur?.requesterFollowing || undefined, - followedBy: cur?.requesterFollowedBy || undefined, + blockedBy: !!bam.blockedBy([viewer, did]), + blocking: bam.blocking([viewer, did]) ?? undefined, + following: prof?.viewerFollowing || undefined, + followedBy: prof?.viewerFollowedBy || undefined, } : undefined, - labels: skipLabels ? undefined : [...actorLabels, ...selfLabels], + labels: [...actorLabels, ...selfLabels], } - acc[cur.did] = profile return acc - }, {} as Record) + }, {} as ProfileViewMap) } - async hydrateProfiles( - results: ActorResult[], - viewer: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const profiles = await this.profiles(results, viewer, opts) - return mapDefined(results, (result) => profiles[result.did]) - } - - async profile( - result: ActorResult, - viewer: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const profiles = await this.profiles([result], viewer, opts) - return profiles[result.did] ?? null - } - - // @NOTE keep in sync with feedService.getActorViews() - async profilesBasic( - results: ActorResult[], - viewer: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise> { - if (results.length === 0) return {} - const profiles = await this.profiles(results, viewer, opts) - return Object.values(profiles).reduce((acc, cur) => { - const profile = { - did: cur.did, - handle: cur.handle, - displayName: cur.displayName, - avatar: cur.avatar, - viewer: cur.viewer, + profileBasicPresentation( + dids: string[], + state: ProfileHydrationState, + opts?: { + viewer?: string | null + omitLabels?: boolean + }, + ): ProfileViewMap { + const result = this.profilePresentation(dids, state, opts) + return Object.values(result).reduce((acc, prof) => { + const profileBasic = { + did: prof.did, + handle: prof.handle, + displayName: prof.displayName, + avatar: prof.avatar, + viewer: prof.viewer, + labels: opts?.omitLabels ? undefined : prof.labels, } - acc[cur.did] = profile + acc[prof.did] = profileBasic return acc - }, {} as Record) - } - - async hydrateProfilesBasic( - results: ActorResult[], - viewer: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const profiles = await this.profilesBasic(results, viewer, opts) - return mapDefined(results, (result) => profiles[result.did]) - } - - async profileBasic( - result: ActorResult, - viewer: string | null, - opts?: { skipLabels?: boolean; includeSoftDeleted?: boolean }, - ): Promise { - const profiles = await this.profilesBasic([result], viewer, opts) - return profiles[result.did] ?? null + }, {} as ProfileViewMap) } } diff --git a/packages/bsky/src/services/feed/index.ts b/packages/bsky/src/services/feed/index.ts index 895cec08c7b..db32f1971bc 100644 --- a/packages/bsky/src/services/feed/index.ts +++ b/packages/bsky/src/services/feed/index.ts @@ -1,15 +1,8 @@ import { sql } from 'kysely' import { AtUri } from '@atproto/syntax' -import { dedupeStrs } from '@atproto/common' -import { INVALID_HANDLE } from '@atproto/syntax' import { jsonStringToLex } from '@atproto/lexicon' import { Database } from '../../db' -import { - countAll, - noMatch, - notSoftDeletedClause, - valuesList, -} from '../../db/util' +import { countAll, noMatch, notSoftDeletedClause } from '../../db/util' import { ImageUriBuilder } from '../../image/uri' import { ids } from '../../lexicon/lexicons' import { @@ -24,26 +17,20 @@ import { } from '../../lexicon/types/app/bsky/embed/record' import { isMain as isEmbedRecordWithMedia } from '../../lexicon/types/app/bsky/embed/recordWithMedia' import { - FeedViewPost, - SkeletonFeedPost, -} from '../../lexicon/types/app/bsky/feed/defs' -import { - ActorInfoMap, PostInfoMap, FeedItemType, FeedRow, FeedGenInfoMap, - PostViews, PostEmbedViews, RecordEmbedViewRecordMap, PostInfo, RecordEmbedViewRecord, PostBlocksMap, - kSelfLabels, + FeedHydrationState, } from './types' -import { LabelService, Labels, getSelfLabels } from '../label' +import { LabelService } from '../label' import { ActorService } from '../actor' -import { GraphService } from '../graph' +import { BlockAndMuteState, GraphService, RelationshipPair } from '../graph' import { FeedViews } from './views' import { LabelCache } from '../../label-cache' @@ -56,7 +43,7 @@ export class FeedService { public labelCache: LabelCache, ) {} - views = new FeedViews(this.db, this.imgUriBuilder) + views = new FeedViews(this.db, this.imgUriBuilder, this.labelCache) services = { label: LabelService.creator(this.labelCache)(this.db), @@ -123,127 +110,6 @@ export class FeedService { ) } - // @TODO just use actor service?? - // @NOTE keep in sync with actorService.views.profile() - async getActorInfos( - dids: string[], - viewer: string | null, - opts?: { skipLabels?: boolean }, // @NOTE used by hydrateFeed() to batch label hydration - ): Promise { - if (dids.length < 1) return {} - const { ref } = this.db.db.dynamic - const { skipLabels } = opts ?? {} - const [actors, labels] = await Promise.all([ - this.db.db - .selectFrom('actor') - .leftJoin('profile', 'profile.creator', 'actor.did') - .leftJoin('record', 'record.uri', 'profile.uri') - .where('actor.did', 'in', dids) - .where(notSoftDeletedClause(ref('actor'))) - .selectAll('actor') - .select([ - 'profile.uri as profileUri', - 'profile.cid as profileCid', - 'profile.displayName as displayName', - 'profile.description as description', - 'profile.avatarCid as avatarCid', - 'profile.indexedAt as indexedAt', - 'record.json as profileJson', - this.db.db - .selectFrom('follow') - .if(!viewer, (q) => q.where(noMatch)) - .where('creator', '=', viewer ?? '') - .whereRef('subjectDid', '=', ref('actor.did')) - .select('uri') - .as('requesterFollowing'), - this.db.db - .selectFrom('follow') - .if(!viewer, (q) => q.where(noMatch)) - .whereRef('creator', '=', ref('actor.did')) - .where('subjectDid', '=', viewer ?? '') - .select('uri') - .as('requesterFollowedBy'), - this.db.db - .selectFrom('actor_block') - .if(!viewer, (q) => q.where(noMatch)) - .where('creator', '=', viewer ?? '') - .whereRef('subjectDid', '=', ref('actor.did')) - .select('uri') - .as('requesterBlocking'), - this.db.db - .selectFrom('actor_block') - .if(!viewer, (q) => q.where(noMatch)) - .whereRef('creator', '=', ref('actor.did')) - .where('subjectDid', '=', viewer ?? '') - .select('uri') - .as('requesterBlockedBy'), - this.db.db - .selectFrom('mute') - .if(!viewer, (q) => q.where(noMatch)) - .whereRef('subjectDid', '=', ref('actor.did')) - .where('mutedByDid', '=', viewer ?? '') - .select('subjectDid') - .as('requesterMuted'), - this.db.db - .selectFrom('list_item') - .if(!viewer, (q) => q.where(noMatch)) - .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') - .where('list_mute.mutedByDid', '=', viewer ?? '') - .whereRef('list_item.subjectDid', '=', ref('actor.did')) - .select('list_item.listUri') - .limit(1) - .as('requesterMutedByList'), - ]) - .execute(), - this.services.label.getLabelsForSubjects(skipLabels ? [] : dids), - ]) - const listUris: string[] = actors - .map((a) => a.requesterMutedByList) - .filter((list) => !!list) - const listViews = await this.services.graph.getListViews(listUris, viewer) - return actors.reduce((acc, cur) => { - const avatar = cur.avatarCid - ? this.imgUriBuilder.getPresetUri('avatar', cur.did, cur.avatarCid) - : undefined - const mutedByList = - cur.requesterMutedByList && listViews[cur.requesterMutedByList] - ? this.services.graph.formatListViewBasic( - listViews[cur.requesterMutedByList], - ) - : undefined - const actorLabels = labels[cur.did] ?? [] - const selfLabels = getSelfLabels({ - uri: cur.profileUri, - cid: cur.profileCid, - record: - cur.profileJson !== null - ? (jsonStringToLex(cur.profileJson) as Record) - : null, - }) - return { - ...acc, - [cur.did]: { - did: cur.did, - handle: cur.handle ?? INVALID_HANDLE, - displayName: cur.displayName ?? undefined, - avatar, - viewer: viewer - ? { - muted: !!cur?.requesterMuted || !!cur?.requesterMutedByList, - mutedByList, - blockedBy: !!cur?.requesterBlockedBy, - blocking: cur?.requesterBlocking || undefined, - following: cur?.requesterFollowing || undefined, - followedBy: cur?.requesterFollowedBy || undefined, - } - : undefined, - labels: skipLabels ? undefined : [...actorLabels, ...selfLabels], - [kSelfLabels]: selfLabels, - }, - } - }, {} as ActorInfoMap) - } - async getPostInfos( postUris: string[], viewer: string | null, @@ -312,102 +178,23 @@ export class FeedService { ) } - async getPostViews( - postUris: string[], - requester: string | null, - precomputed?: { - actors?: ActorInfoMap - posts?: PostInfoMap - embeds?: PostEmbedViews - blocks?: PostBlocksMap - labels?: Labels - }, - ): Promise { - const uris = dedupeStrs(postUris) - const dids = dedupeStrs(postUris.map((uri) => new AtUri(uri).hostname)) - - const [actors, posts, labels] = await Promise.all([ - precomputed?.actors ?? - this.getActorInfos(dids, requester, { skipLabels: true }), - precomputed?.posts ?? this.getPostInfos(uris, requester), - precomputed?.labels ?? - this.services.label.getLabelsForSubjects([...uris, ...dids]), - ]) - const blocks = precomputed?.blocks ?? (await this.blocksForPosts(posts)) - const embeds = - precomputed?.embeds ?? - (await this.embedsForPosts(posts, blocks, requester)) - - return uris.reduce((acc, cur) => { - const view = this.views.formatPostView(cur, actors, posts, embeds, labels) - if (view) { - acc[cur] = view - } - return acc - }, {} as PostViews) - } - - async filterAndGetFeedItems( - uris: string[], - requester: string, - ): Promise> { + async getFeedItems(uris: string[]): Promise> { if (uris.length < 1) return {} - const { ref } = this.db.db.dynamic const feedItems = await this.selectFeedItemQb() .where('feed_item.uri', 'in', uris) - .where((qb) => - // Hide posts and reposts of or by muted actors - this.services.graph.whereNotMuted(qb, requester, [ - ref('post.creator'), - ref('originatorDid'), - ]), - ) - .whereNotExists( - this.services.graph.blockQb(requester, [ - ref('post.creator'), - ref('originatorDid'), - ]), - ) .execute() return feedItems.reduce((acc, item) => { return Object.assign(acc, { [item.uri]: item }) }, {} as Record) } - // @TODO enforce limit elsewhere - async cleanFeedSkeleton( - skeleton: SkeletonFeedPost[], - limit: number, - requester: string, - ): Promise { - skeleton = skeleton.slice(0, limit) - const feedItemUris = skeleton.map(getSkeleFeedItemUri) - const feedItems = await this.filterAndGetFeedItems(feedItemUris, requester) - - const cleaned: FeedRow[] = [] - for (const skeleItem of skeleton) { - const feedItem = feedItems[getSkeleFeedItemUri(skeleItem)] - if (feedItem && feedItem.postUri === skeleItem.post) { - cleaned.push(feedItem) - } - } - return cleaned - } - - async hydrateFeed( - items: FeedRow[], - viewer: string | null, - // @TODO (deprecated) remove this once all clients support the blocked/not-found union on post views - usePostViewUnion?: boolean, - ): Promise { + feedItemRefs(items: FeedRow[]) { const actorDids = new Set() const postUris = new Set() for (const item of items) { - actorDids.add(item.postAuthorDid) postUris.add(item.postUri) - if (item.postAuthorDid !== item.originatorDid) { - actorDids.add(item.originatorDid) - } + actorDids.add(item.postAuthorDid) + actorDids.add(item.originatorDid) if (item.replyParent) { postUris.add(item.replyParent) actorDids.add(new AtUri(item.replyParent).hostname) @@ -417,29 +204,51 @@ export class FeedService { actorDids.add(new AtUri(item.replyRoot).hostname) } } - const [actors, posts, labels] = await Promise.all([ - this.getActorInfos(Array.from(actorDids), viewer, { - skipLabels: true, - }), - this.getPostInfos(Array.from(postUris), viewer), - this.services.label.getLabelsForSubjects([...postUris, ...actorDids]), - ]) - const blocks = await this.blocksForPosts(posts) - const embeds = await this.embedsForPosts(posts, blocks, viewer) + return { dids: actorDids, uris: postUris } + } - return this.views.formatFeed( - items, - actors, + async feedHydration( + refs: { + dids: Set + uris: Set + viewer: string | null + }, + depth = 0, + ): Promise { + const { viewer, dids, uris } = refs + const [posts, labels, bam] = await Promise.all([ + this.getPostInfos(Array.from(uris), viewer), + this.services.label.getLabelsForSubjects([...uris, ...dids]), + this.services.graph.getBlockAndMuteState( + viewer ? [...dids].map((did) => [viewer, did]) : [], + ), + ]) + // profileState for labels and bam handled above, profileHydration() shouldn't fetch additional + const [profileState, blocks] = await Promise.all([ + this.services.actor.views.profileHydration( + Array.from(dids), + { viewer }, + { bam, labels }, + ), + this.blocksForPosts(posts, bam), + ]) + const embeds = await this.embedsForPosts(posts, blocks, viewer, depth) + return { posts, - embeds, - labels, blocks, - usePostViewUnion, - ) + embeds, + labels, // includes info for profiles + bam, // includes info for profiles + profiles: profileState.profiles, + lists: profileState.lists, + } } // applies blocks for visibility to third-parties (i.e. based on post content) - async blocksForPosts(posts: PostInfoMap): Promise { + async blocksForPosts( + posts: PostInfoMap, + bam?: BlockAndMuteState, + ): Promise { const relationships: RelationshipPair[] = [] const byPost: Record = {} const didFromUri = (uri) => new AtUri(uri).host @@ -466,15 +275,17 @@ export class FeedService { } } // compute block state from all actor relationships among posts - const blockSet = await this.getBlockSet(relationships) - if (blockSet.empty()) return {} + const blockState = await this.services.graph.getBlockState( + relationships, + bam, + ) const result: PostBlocksMap = {} Object.entries(byPost).forEach(([uri, block]) => { - if (block.embed && blockSet.has(block.embed)) { + if (block.embed && blockState.block(block.embed)) { result[uri] ??= {} result[uri].embed = true } - if (block.reply && blockSet.has(block.reply)) { + if (block.reply && blockState.block(block.reply)) { result[uri] ??= {} result[uri].reply = true } @@ -482,33 +293,11 @@ export class FeedService { return result } - private async getBlockSet(relationships: RelationshipPair[]) { - const { ref } = this.db.db.dynamic - const blockSet = new RelationshipSet() - if (!relationships.length) return blockSet - const relationshipSet = new RelationshipSet() - relationships.forEach((pair) => relationshipSet.add(pair)) - // compute actual block set from all actor relationships - const blockRows = await this.db.db - .selectFrom('actor_block') - .select(['creator', 'subjectDid']) // index-only columns - .where( - sql`(${ref('creator')}, ${ref('subjectDid')})`, - 'in', - valuesList( - relationshipSet.listAllPairs().map(([a, b]) => sql`${a}, ${b}`), - ), - ) - .execute() - blockRows.forEach((r) => blockSet.add([r.creator, r.subjectDid])) - return blockSet - } - async embedsForPosts( postInfos: PostInfoMap, blocks: PostBlocksMap, viewer: string | null, - depth = 0, + depth: number, ) { const postMap = postRecordsFromInfos(postInfos) const posts = Object.values(postMap) @@ -559,39 +348,37 @@ export class FeedService { ): Promise { const nestedUris = nestedRecordUris(posts) if (nestedUris.length < 1) return {} - const nestedPostUris: string[] = [] - const nestedFeedGenUris: string[] = [] - const nestedListUris: string[] = [] - const nestedDidsSet = new Set() + const nestedDids = new Set() + const nestedPostUris = new Set() + const nestedFeedGenUris = new Set() + const nestedListUris = new Set() for (const uri of nestedUris) { const parsed = new AtUri(uri) - nestedDidsSet.add(parsed.hostname) + nestedDids.add(parsed.hostname) if (parsed.collection === ids.AppBskyFeedPost) { - nestedPostUris.push(uri) + nestedPostUris.add(uri) } else if (parsed.collection === ids.AppBskyFeedGenerator) { - nestedFeedGenUris.push(uri) + nestedFeedGenUris.add(uri) } else if (parsed.collection === ids.AppBskyGraphList) { - nestedListUris.push(uri) + nestedListUris.add(uri) } } - const nestedDids = [...nestedDidsSet] - const [postInfos, actorInfos, labelViews, feedGenInfos, listViews] = - await Promise.all([ - this.getPostInfos(nestedPostUris, viewer), - this.getActorInfos(nestedDids, viewer, { skipLabels: true }), - this.services.label.getLabelsForSubjects([ - ...nestedPostUris, - ...nestedDids, - ]), - this.getFeedGeneratorInfos(nestedFeedGenUris, viewer), - this.services.graph.getListViews(nestedListUris, viewer), - ]) - const deepBlocks = await this.blocksForPosts(postInfos) - const deepEmbedViews = await this.embedsForPosts( - postInfos, - deepBlocks, - viewer, - depth + 1, + const [feedState, feedGenInfos, listViews] = await Promise.all([ + this.feedHydration( + { + dids: nestedDids, + uris: nestedPostUris, + viewer, + }, + depth + 1, + ), + this.getFeedGeneratorInfos([...nestedFeedGenUris], viewer), + this.services.graph.getListViews([...nestedListUris], viewer), + ]) + const actorInfos = this.services.actor.views.profileBasicPresentation( + [...nestedDids], + feedState, + { viewer }, ) const recordEmbedViews: RecordEmbedViewRecordMap = {} for (const uri of nestedUris) { @@ -599,24 +386,20 @@ export class FeedService { if (collection === ids.AppBskyFeedGenerator && feedGenInfos[uri]) { recordEmbedViews[uri] = { $type: 'app.bsky.feed.defs#generatorView', - ...this.views.formatFeedGeneratorView( - feedGenInfos[uri], - actorInfos, - labelViews, - ), + ...this.views.formatFeedGeneratorView(feedGenInfos[uri], actorInfos), } } else if (collection === ids.AppBskyGraphList && listViews[uri]) { recordEmbedViews[uri] = { $type: 'app.bsky.graph.defs#listView', ...this.services.graph.formatListView(listViews[uri], actorInfos), } - } else if (collection === ids.AppBskyFeedPost && postInfos[uri]) { + } else if (collection === ids.AppBskyFeedPost && feedState.posts[uri]) { const formatted = this.views.formatPostView( uri, actorInfos, - postInfos, - deepEmbedViews, - labelViews, + feedState.posts, + feedState.embeds, + feedState.labels, ) recordEmbedViews[uri] = this.views.getRecordEmbedView( uri, @@ -664,35 +447,6 @@ const nestedRecordUris = (posts: PostRecord[]): string[] => { type PostRelationships = { reply?: RelationshipPair; embed?: RelationshipPair } -type RelationshipPair = [didA: string, didB: string] - -class RelationshipSet { - index = new Map>() - add([didA, didB]: RelationshipPair) { - const didAIdx = this.index.get(didA) ?? new Set() - const didBIdx = this.index.get(didB) ?? new Set() - if (!this.index.has(didA)) this.index.set(didA, didAIdx) - if (!this.index.has(didB)) this.index.set(didB, didBIdx) - didAIdx.add(didB) - didBIdx.add(didA) - } - has([didA, didB]: RelationshipPair) { - return !!this.index.get(didA)?.has(didB) - } - listAllPairs() { - const pairs: RelationshipPair[] = [] - for (const [didA, didBIdx] of this.index.entries()) { - for (const didB of didBIdx) { - pairs.push([didA, didB]) - } - } - return pairs - } - empty() { - return this.index.size === 0 - } -} - function applyEmbedBlock( uri: string, blocks: PostBlocksMap, @@ -716,9 +470,3 @@ function applyEmbedBlock( } return view } - -function getSkeleFeedItemUri(item: SkeletonFeedPost) { - return typeof item.reason?.repost === 'string' - ? item.reason.repost - : item.post -} diff --git a/packages/bsky/src/services/feed/types.ts b/packages/bsky/src/services/feed/types.ts index f1b5500c62e..894ee0a564f 100644 --- a/packages/bsky/src/services/feed/types.ts +++ b/packages/bsky/src/services/feed/types.ts @@ -14,9 +14,11 @@ import { NotFoundPost, PostView, } from '../../lexicon/types/app/bsky/feed/defs' -import { Label } from '../../lexicon/types/com/atproto/label/defs' import { FeedGenerator } from '../../db/tables/feed-generator' import { ListView } from '../../lexicon/types/app/bsky/graph/defs' +import { ProfileHydrationState } from '../actor' +import { Labels } from '../label' +import { BlockAndMuteState } from '../graph' export type PostEmbedViews = { [uri: string]: PostEmbedView @@ -28,8 +30,6 @@ export type PostEmbedView = | RecordEmbedView | RecordWithMediaEmbedView -export type PostViews = { [uri: string]: PostView } - export type PostInfo = { uri: string cid: string @@ -50,26 +50,6 @@ export type PostBlocksMap = { [uri: string]: { reply?: boolean; embed?: boolean } } -export const kSelfLabels = Symbol('selfLabels') - -export type ActorInfo = { - did: string - handle: string - displayName?: string - avatar?: string - viewer?: { - muted?: boolean - blockedBy?: boolean - blocking?: string - following?: string - followedBy?: string - } - labels?: Label[] - // allows threading self-labels through if they are going to be applied later, i.e. when using skipLabels option. - [kSelfLabels]?: Label[] -} -export type ActorInfoMap = { [did: string]: ActorInfo } - export type FeedGenInfo = Selectable & { likeCount: number viewer?: { @@ -103,3 +83,11 @@ export type RecordEmbedViewRecord = | ListView export type RecordEmbedViewRecordMap = { [uri: string]: RecordEmbedViewRecord } + +export type FeedHydrationState = ProfileHydrationState & { + posts: PostInfoMap + embeds: PostEmbedViews + labels: Labels + blocks: PostBlocksMap + bam: BlockAndMuteState +} diff --git a/packages/bsky/src/services/feed/views.ts b/packages/bsky/src/services/feed/views.ts index dc19ddf637e..439e68f3d1f 100644 --- a/packages/bsky/src/services/feed/views.ts +++ b/packages/bsky/src/services/feed/views.ts @@ -21,7 +21,6 @@ import { ViewRecord, } from '../../lexicon/types/app/bsky/embed/record' import { - ActorInfoMap, PostEmbedViews, FeedGenInfo, FeedRow, @@ -29,31 +28,33 @@ import { PostInfoMap, RecordEmbedViewRecord, PostBlocksMap, - kSelfLabels, + FeedHydrationState, } from './types' import { Labels, getSelfLabels } from '../label' import { ImageUriBuilder } from '../../image/uri' +import { LabelCache } from '../../label-cache' +import { ActorInfoMap, ActorService } from '../actor' export class FeedViews { - constructor(public db: Database, public imgUriBuilder: ImageUriBuilder) {} + constructor( + public db: Database, + public imgUriBuilder: ImageUriBuilder, + public labelCache: LabelCache, + ) {} - static creator(imgUriBuilder: ImageUriBuilder) { - return (db: Database) => new FeedViews(db, imgUriBuilder) + static creator(imgUriBuilder: ImageUriBuilder, labelCache: LabelCache) { + return (db: Database) => new FeedViews(db, imgUriBuilder, labelCache) + } + + services = { + actor: ActorService.creator(this.imgUriBuilder, this.labelCache)(this.db), } formatFeedGeneratorView( info: FeedGenInfo, profiles: ActorInfoMap, - labels?: Labels, ): GeneratorView { const profile = profiles[info.creator] - if (profile && !profile.labels) { - // If the creator labels are not hydrated yet, attempt to pull them - // from labels: e.g. compatible with embedsForPosts() batching label hydration. - const profileLabels = labels?.[info.creator] ?? [] - const profileSelfLabels = profile[kSelfLabels] ?? [] - profile.labels = [...profileLabels, ...profileSelfLabels] - } return { uri: info.uri, cid: info.cid, @@ -83,13 +84,18 @@ export class FeedViews { formatFeed( items: FeedRow[], - actors: ActorInfoMap, - posts: PostInfoMap, - embeds: PostEmbedViews, - labels: Labels, - blocks: PostBlocksMap, - usePostViewUnion?: boolean, + state: FeedHydrationState, + opts?: { + viewer?: string | null + usePostViewUnion?: boolean + }, ): FeedViewPost[] { + const { posts, profiles, blocks, embeds, labels } = state + const actors = this.services.actor.views.profileBasicPresentation( + Object.keys(profiles), + state, + opts, + ) const feed: FeedViewPost[] = [] for (const item of items) { const post = this.formatPostView( @@ -110,14 +116,9 @@ export class FeedViews { if (!originator) { continue } else { - const originatorLabels = labels[item.originatorDid] ?? [] - const originatorSelfLabels = originator[kSelfLabels] ?? [] feedPost['reason'] = { $type: 'app.bsky.feed.defs#reasonRepost', - by: { - ...originator, - labels: [...originatorLabels, ...originatorSelfLabels], - }, + by: originator, indexedAt: item.sortAt, } } @@ -130,7 +131,7 @@ export class FeedViews { embeds, labels, blocks, - usePostViewUnion, + opts, ) const replyRoot = this.formatMaybePostView( item.replyRoot, @@ -139,7 +140,7 @@ export class FeedViews { embeds, labels, blocks, - usePostViewUnion, + opts, ) if (replyRoot && replyParent) { feedPost['reply'] = { @@ -163,11 +164,6 @@ export class FeedViews { const post = posts[uri] const author = actors[post?.creator] if (!post || !author) return undefined - // If the author labels are not hydrated yet, attempt to pull them - // from labels: e.g. compatible with hydrateFeed() batching label hydration. - const authorLabels = labels[author.did] ?? [] - const authorSelfLabels = author[kSelfLabels] ?? [] - author.labels ??= [...authorLabels, ...authorSelfLabels] const postLabels = labels[uri] ?? [] const postSelfLabels = getSelfLabels({ uri: post.uri, @@ -201,11 +197,13 @@ export class FeedViews { embeds: PostEmbedViews, labels: Labels, blocks: PostBlocksMap, - usePostViewUnion?: boolean, + opts?: { + usePostViewUnion?: boolean + }, ): MaybePostView | undefined { const post = this.formatPostView(uri, actors, posts, embeds, labels) if (!post) { - if (!usePostViewUnion) return + if (!opts?.usePostViewUnion) return return this.notFoundPost(uri) } if ( @@ -213,7 +211,7 @@ export class FeedViews { post.author.viewer?.blocking || blocks[uri]?.reply ) { - if (!usePostViewUnion) return + if (!opts?.usePostViewUnion) return return this.blockedPost(post) } return { diff --git a/packages/bsky/src/services/graph/index.ts b/packages/bsky/src/services/graph/index.ts index 4c05dc7cecd..53592ac4021 100644 --- a/packages/bsky/src/services/graph/index.ts +++ b/packages/bsky/src/services/graph/index.ts @@ -1,10 +1,11 @@ +import { sql } from 'kysely' import { Database } from '../../db' import { ImageUriBuilder } from '../../image/uri' -import { ProfileView } from '../../lexicon/types/app/bsky/actor/defs' -import { List } from '../../db/tables/list' -import { Selectable, WhereInterface, sql } from 'kysely' -import { NotEmptyArray } from '@atproto/common' -import { DbRef, noMatch } from '../../db/util' +import { valuesList } from '../../db/util' +import { ListInfo } from './types' +import { ActorInfoMap } from '../actor' + +export * from './types' export class GraphService { constructor(public db: Database, public imgUriBuilder: ImageUriBuilder) {} @@ -69,27 +70,6 @@ export class GraphService { .execute() } - whereNotMuted>( - qb: W, - requester: string, - refs: NotEmptyArray, - ) { - const subjectRefs = sql.join(refs) - const actorMute = this.db.db - .selectFrom('mute') - .where('mutedByDid', '=', requester) - .where('subjectDid', 'in', sql`(${subjectRefs})`) - .select('subjectDid as muted') - const listMute = this.db.db - .selectFrom('list_item') - .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') - .where('list_mute.mutedByDid', '=', requester) - .whereRef('list_item.subjectDid', 'in', sql`(${subjectRefs})`) - .select('list_item.subjectDid as muted') - // Splitting the mute from list-mute checks seems to be more flexible for the query-planner and often quicker - return qb.whereNotExists(actorMute).whereNotExists(listMute) - } - getListsQb(viewer: string | null) { const { ref } = this.db.db.dynamic return this.db.db @@ -98,14 +78,20 @@ export class GraphService { .selectAll('list') .selectAll('actor') .select('list.sortAt as sortAt') - .select( + .select([ this.db.db .selectFrom('list_mute') .where('list_mute.mutedByDid', '=', viewer ?? '') .whereRef('list_mute.listUri', '=', ref('list.uri')) .select('list_mute.listUri') .as('viewerMuted'), - ) + this.db.db + .selectFrom('list_block') + .where('list_block.creator', '=', viewer ?? '') + .whereRef('list_block.subjectUri', '=', ref('list.uri')) + .select('list_block.uri') + .as('viewerListBlockUri'), + ]) } getListItemsQb() { @@ -116,84 +102,117 @@ export class GraphService { .select(['list_item.cid as cid', 'list_item.sortAt as sortAt']) } - blockQb(viewer: string | null, refs: NotEmptyArray) { - const subjectRefs = sql.join(refs) - return this.db.db - .selectFrom('actor_block') - .if(!viewer, (q) => q.where(noMatch)) - .where((outer) => - outer - .where((qb) => - qb - .where('actor_block.creator', '=', viewer ?? '') - .whereRef('actor_block.subjectDid', 'in', sql`(${subjectRefs})`), - ) - .orWhere((qb) => - qb - .where('actor_block.subjectDid', '=', viewer ?? '') - .whereRef('actor_block.creator', 'in', sql`(${subjectRefs})`), - ), - ) - .select(['creator', 'subjectDid']) - } - - blockRefQb(first: DbRef, second: DbRef) { - return this.db.db - .selectFrom('actor_block') - .where((outer) => - outer - .where((qb) => - qb - .whereRef('actor_block.creator', '=', first) - .whereRef('actor_block.subjectDid', '=', second), - ) - .orWhere((qb) => - qb - .whereRef('actor_block.subjectDid', '=', first) - .whereRef('actor_block.creator', '=', second), - ), - ) - .select(['creator', 'subjectDid']) + async getBlockAndMuteState( + pairs: RelationshipPair[], + bam?: BlockAndMuteState, + ) { + pairs = bam ? pairs.filter((pair) => !bam.has(pair)) : pairs + const result = bam ?? new BlockAndMuteState() + if (!pairs.length) return result + const { ref } = this.db.db.dynamic + const sourceRef = ref('pair.source') + const targetRef = ref('pair.target') + const values = valuesList(pairs.map((p) => sql`${p[0]}, ${p[1]}`)) + const items = await this.db.db + .selectFrom(values.as(sql`pair (source, target)`)) + .select([ + sql`${sourceRef}`.as('source'), + sql`${targetRef}`.as('target'), + this.db.db + .selectFrom('actor_block') + .whereRef('creator', '=', sourceRef) + .whereRef('subjectDid', '=', targetRef) + .select('uri') + .as('blocking'), + this.db.db + .selectFrom('list_item') + .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') + .whereRef('list_block.creator', '=', sourceRef) + .whereRef('list_item.subjectDid', '=', targetRef) + .select('list_item.listUri') + .limit(1) + .as('blockingViaList'), + this.db.db + .selectFrom('actor_block') + .whereRef('creator', '=', targetRef) + .whereRef('subjectDid', '=', sourceRef) + .select('uri') + .as('blockedBy'), + this.db.db + .selectFrom('list_item') + .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') + .whereRef('list_block.creator', '=', targetRef) + .whereRef('list_item.subjectDid', '=', sourceRef) + .select('list_item.listUri') + .limit(1) + .as('blockedByViaList'), + this.db.db + .selectFrom('mute') + .whereRef('mutedByDid', '=', sourceRef) + .whereRef('subjectDid', '=', targetRef) + .select(sql`${true}`.as('val')) + .as('muting'), + this.db.db + .selectFrom('list_item') + .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') + .whereRef('list_mute.mutedByDid', '=', sourceRef) + .whereRef('list_item.subjectDid', '=', targetRef) + .select('list_item.listUri') + .limit(1) + .as('mutingViaList'), + ]) + .selectAll() + .execute() + items.forEach((item) => result.add(item)) + return result } - async getBlocks( - requester: string, - subjectHandleOrDid: string, - ): Promise<{ blocking: boolean; blockedBy: boolean }> { - let subjectDid - if (subjectHandleOrDid.startsWith('did:')) { - subjectDid = subjectHandleOrDid - } else { - const res = await this.db.db - .selectFrom('actor') - .where('handle', '=', subjectHandleOrDid) - .select('did') - .executeTakeFirst() - if (!res) { - return { blocking: false, blockedBy: false } - } - subjectDid = res.did - } - - const accnts = [requester, subjectDid] - const blockRes = await this.db.db - .selectFrom('actor_block') - .where('creator', 'in', accnts) - .where('subjectDid', 'in', accnts) + async getBlockState(pairs: RelationshipPair[], bam?: BlockAndMuteState) { + pairs = bam ? pairs.filter((pair) => !bam.has(pair)) : pairs + const result = bam ?? new BlockAndMuteState() + if (!pairs.length) return result + const { ref } = this.db.db.dynamic + const sourceRef = ref('pair.source') + const targetRef = ref('pair.target') + const values = valuesList(pairs.map((p) => sql`${p[0]}, ${p[1]}`)) + const items = await this.db.db + .selectFrom(values.as(sql`pair (source, target)`)) + .select([ + sql`${sourceRef}`.as('source'), + sql`${targetRef}`.as('target'), + this.db.db + .selectFrom('actor_block') + .whereRef('creator', '=', sourceRef) + .whereRef('subjectDid', '=', targetRef) + .select('uri') + .as('blocking'), + this.db.db + .selectFrom('list_item') + .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') + .whereRef('list_block.creator', '=', sourceRef) + .whereRef('list_item.subjectDid', '=', targetRef) + .select('list_item.listUri') + .limit(1) + .as('blockingViaList'), + this.db.db + .selectFrom('actor_block') + .whereRef('creator', '=', targetRef) + .whereRef('subjectDid', '=', sourceRef) + .select('uri') + .as('blockedBy'), + this.db.db + .selectFrom('list_item') + .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') + .whereRef('list_block.creator', '=', targetRef) + .whereRef('list_item.subjectDid', '=', sourceRef) + .select('list_item.listUri') + .limit(1) + .as('blockedByViaList'), + ]) .selectAll() .execute() - - const blocking = blockRes.some( - (row) => row.creator === requester && row.subjectDid === subjectDid, - ) - const blockedBy = blockRes.some( - (row) => row.creator === subjectDid && row.subjectDid === requester, - ) - - return { - blocking, - blockedBy, - } + items.forEach((item) => result.add(item)) + return result } async getListViews(listUris: string[], requester: string | null) { @@ -210,28 +229,14 @@ export class GraphService { ) } - formatListView(list: ListInfo, profiles: Record) { + formatListView(list: ListInfo, profiles: ActorInfoMap) { return { - uri: list.uri, - cid: list.cid, + ...this.formatListViewBasic(list), creator: profiles[list.creator], - name: list.name, - purpose: list.purpose, description: list.description ?? undefined, descriptionFacets: list.descriptionFacets ? JSON.parse(list.descriptionFacets) : undefined, - avatar: list.avatarCid - ? this.imgUriBuilder.getPresetUri( - 'avatar', - list.creator, - list.avatarCid, - ) - : undefined, - indexedAt: list.sortAt, - viewer: { - muted: !!list.viewerMuted, - }, } } @@ -248,14 +253,92 @@ export class GraphService { list.avatarCid, ) : undefined, - indexedAt: list.indexedAt, + indexedAt: list.sortAt, viewer: { muted: !!list.viewerMuted, + blocked: list.viewerListBlockUri ?? undefined, }, } } } -type ListInfo = Selectable & { - viewerMuted: string | null +export type RelationshipPair = [didA: string, didB: string] + +export class BlockAndMuteState { + hasIdx = new Map>() // did -> did + blockIdx = new Map>() // did -> did -> block uri + muteIdx = new Map>() // did -> did + muteListIdx = new Map>() // did -> did -> list uri + constructor(items: BlockAndMuteInfo[] = []) { + items.forEach((item) => this.add(item)) + } + add(item: BlockAndMuteInfo) { + const blocking = item.blocking || item.blockingViaList // block or list uri + if (blocking) { + const map = this.blockIdx.get(item.source) ?? new Map() + map.set(item.target, blocking) + if (!this.blockIdx.has(item.source)) { + this.blockIdx.set(item.source, map) + } + } + const blockedBy = item.blockedBy || item.blockedByViaList // block or list uri + if (blockedBy) { + const map = this.blockIdx.get(item.target) ?? new Map() + map.set(item.source, blockedBy) + if (!this.blockIdx.has(item.target)) { + this.blockIdx.set(item.target, map) + } + } + if (item.muting) { + const set = this.muteIdx.get(item.source) ?? new Set() + set.add(item.target) + if (!this.muteIdx.has(item.source)) { + this.muteIdx.set(item.source, set) + } + } + if (item.mutingViaList) { + const map = this.muteListIdx.get(item.source) ?? new Map() + map.set(item.target, item.mutingViaList) + if (!this.muteListIdx.has(item.source)) { + this.muteListIdx.set(item.source, map) + } + } + const set = this.hasIdx.get(item.source) ?? new Set() + set.add(item.target) + if (!this.hasIdx.has(item.source)) { + this.hasIdx.set(item.source, set) + } + } + block(pair: RelationshipPair): boolean { + return !!this.blocking(pair) || !!this.blockedBy(pair) + } + // block or list uri + blocking(pair: RelationshipPair): string | null { + return this.blockIdx.get(pair[0])?.get(pair[1]) ?? null + } + // block or list uri + blockedBy(pair: RelationshipPair): string | null { + return this.blocking([pair[1], pair[0]]) + } + mute(pair: RelationshipPair): boolean { + return !!this.muteIdx.get(pair[0])?.has(pair[1]) || !!this.muteList(pair) + } + // list uri + muteList(pair: RelationshipPair): string | null { + return this.muteListIdx.get(pair[0])?.get(pair[1]) ?? null + } + has(pair: RelationshipPair) { + return !!this.hasIdx.get(pair[0])?.has(pair[1]) + } +} + +type BlockAndMuteInfo = { + source: string + target: string + blocking?: string | null + blockingViaList?: string | null + blockedBy?: string | null + blockedByViaList?: string | null + muting?: true | null + mutingViaList?: string | null } diff --git a/packages/bsky/src/services/graph/types.ts b/packages/bsky/src/services/graph/types.ts new file mode 100644 index 00000000000..f5ee0c13026 --- /dev/null +++ b/packages/bsky/src/services/graph/types.ts @@ -0,0 +1,9 @@ +import { Selectable } from 'kysely' +import { List } from '../../db/tables/list' + +export type ListInfo = Selectable & { + viewerMuted: string | null + viewerListBlockUri: string | null +} + +export type ListInfoMap = Record diff --git a/packages/bsky/src/services/indexing/index.ts b/packages/bsky/src/services/indexing/index.ts index 33f0a2577c2..05e591c92c4 100644 --- a/packages/bsky/src/services/indexing/index.ts +++ b/packages/bsky/src/services/indexing/index.ts @@ -20,6 +20,7 @@ import * as Follow from './plugins/follow' import * as Profile from './plugins/profile' import * as List from './plugins/list' import * as ListItem from './plugins/list-item' +import * as ListBlock from './plugins/list-block' import * as Block from './plugins/block' import * as FeedGenerator from './plugins/feed-generator' import RecordProcessor from './processor' @@ -39,6 +40,7 @@ export class IndexingService { profile: Profile.PluginType list: List.PluginType listItem: ListItem.PluginType + listBlock: ListBlock.PluginType block: Block.PluginType feedGenerator: FeedGenerator.PluginType } @@ -58,6 +60,7 @@ export class IndexingService { profile: Profile.makePlugin(this.db, backgroundQueue, notifServer), list: List.makePlugin(this.db, backgroundQueue, notifServer), listItem: ListItem.makePlugin(this.db, backgroundQueue, notifServer), + listBlock: ListBlock.makePlugin(this.db, backgroundQueue, notifServer), block: Block.makePlugin(this.db, backgroundQueue, notifServer), feedGenerator: FeedGenerator.makePlugin( this.db, @@ -334,6 +337,10 @@ export class IndexingService { .deleteFrom('actor_block') .where('creator', '=', did) .execute() + await this.db.db + .deleteFrom('list_block') + .where('creator', '=', did) + .execute() // posts const postByUser = (qb) => qb diff --git a/packages/bsky/src/services/indexing/plugins/list-block.ts b/packages/bsky/src/services/indexing/plugins/list-block.ts new file mode 100644 index 00000000000..4285ca8d4bc --- /dev/null +++ b/packages/bsky/src/services/indexing/plugins/list-block.ts @@ -0,0 +1,90 @@ +import { Selectable } from 'kysely' +import { AtUri } from '@atproto/syntax' +import { CID } from 'multiformats/cid' +import * as ListBlock from '../../../lexicon/types/app/bsky/graph/listblock' +import * as lex from '../../../lexicon/lexicons' +import { PrimaryDatabase } from '../../../db' +import { DatabaseSchema, DatabaseSchemaType } from '../../../db/database-schema' +import RecordProcessor from '../processor' +import { BackgroundQueue } from '../../../background' +import { NotificationServer } from '../../../notifications' +import { toSimplifiedISOSafe } from '../util' + +const lexId = lex.ids.AppBskyGraphListblock +type IndexedListBlock = Selectable + +const insertFn = async ( + db: DatabaseSchema, + uri: AtUri, + cid: CID, + obj: ListBlock.Record, + timestamp: string, +): Promise => { + const inserted = await db + .insertInto('list_block') + .values({ + uri: uri.toString(), + cid: cid.toString(), + creator: uri.host, + subjectUri: obj.subject, + createdAt: toSimplifiedISOSafe(obj.createdAt), + indexedAt: timestamp, + }) + .onConflict((oc) => oc.doNothing()) + .returningAll() + .executeTakeFirst() + return inserted || null +} + +const findDuplicate = async ( + db: DatabaseSchema, + uri: AtUri, + obj: ListBlock.Record, +): Promise => { + const found = await db + .selectFrom('list_block') + .where('creator', '=', uri.host) + .where('subjectUri', '=', obj.subject) + .selectAll() + .executeTakeFirst() + return found ? new AtUri(found.uri) : null +} + +const notifsForInsert = () => { + return [] +} + +const deleteFn = async ( + db: DatabaseSchema, + uri: AtUri, +): Promise => { + const deleted = await db + .deleteFrom('list_block') + .where('uri', '=', uri.toString()) + .returningAll() + .executeTakeFirst() + return deleted || null +} + +const notifsForDelete = () => { + return { notifs: [], toDelete: [] } +} + +export type PluginType = RecordProcessor + +export const makePlugin = ( + db: PrimaryDatabase, + backgroundQueue: BackgroundQueue, + notifServer?: NotificationServer, +): PluginType => { + return new RecordProcessor(db, backgroundQueue, notifServer, { + lexId, + insertFn, + findDuplicate, + deleteFn, + notifsForInsert, + notifsForDelete, + }) +} + +export default makePlugin diff --git a/packages/bsky/src/services/label/index.ts b/packages/bsky/src/services/label/index.ts index bfd037e8fb8..855038ab14c 100644 --- a/packages/bsky/src/services/label/index.ts +++ b/packages/bsky/src/services/label/index.ts @@ -100,9 +100,11 @@ export class LabelService { includeNeg?: boolean skipCache?: boolean }, + labels: Labels = {}, ): Promise { - if (subjects.length < 1) return {} + if (subjects.length < 1) return labels const expandedSubjects = subjects.flatMap((subject) => { + if (labels[subject]) return [] // skip over labels we already have fetched if (subject.startsWith('did:')) { return [ subject, @@ -111,8 +113,8 @@ export class LabelService { } return subject }) - const labels = await this.getLabelsForUris(expandedSubjects, opts) - return Object.keys(labels).reduce((acc, cur) => { + const labelsByUri = await this.getLabelsForUris(expandedSubjects, opts) + return Object.keys(labelsByUri).reduce((acc, cur) => { const uri = cur.startsWith('at://') ? new AtUri(cur) : null if ( uri && @@ -122,12 +124,12 @@ export class LabelService { // combine labels for profile + did const did = uri.hostname acc[did] ??= [] - acc[did].push(...labels[cur]) + acc[did].push(...labelsByUri[cur]) } acc[cur] ??= [] - acc[cur].push(...labels[cur]) + acc[cur].push(...labelsByUri[cur]) return acc - }, {} as Labels) + }, labels) } async getLabels( diff --git a/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap b/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap new file mode 100644 index 00000000000..fae6e7f4fa9 --- /dev/null +++ b/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap @@ -0,0 +1,557 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`pds views with blocking from block lists blocks record embeds 1`] = ` +Object { + "thread": Object { + "$type": "app.bsky.feed.defs#threadViewPost", + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(1)", + "muted": false, + }, + }, + "cid": "cids(0)", + "embed": Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewRecord", + "author": Object { + "did": "user(2)", + "handle": "dan.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "cid": "cids(3)", + "embeds": Array [ + Object { + "$type": "app.bsky.embed.record#view", + "record": Object { + "$type": "app.bsky.embed.record#viewBlocked", + "author": Object { + "did": "user(3)", + "viewer": Object { + "blockedBy": false, + "blocking": "record(5)", + }, + }, + "blocked": true, + "uri": "record(4)", + }, + }, + ], + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "uri": "record(3)", + "value": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(4)", + "uri": "record(4)", + }, + }, + "facets": Array [ + Object { + "features": Array [ + Object { + "$type": "app.bsky.richtext.facet#mention", + "did": "user(0)", + }, + ], + "index": Object { + "byteEnd": 18, + "byteStart": 0, + }, + }, + ], + "text": "@alice.bluesky.xyz is the best", + }, + }, + }, + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(0)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "record(0)", + "val": "test-label", + }, + ], + "likeCount": 2, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "embed": Object { + "$type": "app.bsky.embed.record", + "record": Object { + "cid": "cids(3)", + "uri": "record(3)", + }, + }, + "text": "yoohoo label_me", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(0)", + "viewer": Object {}, + }, + }, +} +`; + +exports[`pds views with blocking from block lists blocks thread parent 1`] = ` +Object { + "thread": Object { + "$type": "app.bsky.feed.defs#threadViewPost", + "parent": Object { + "$type": "app.bsky.feed.defs#blockedPost", + "author": Object { + "did": "user(2)", + "viewer": Object { + "blockedBy": true, + }, + }, + "blocked": true, + "uri": "record(4)", + }, + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(2)", + "following": "record(1)", + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 0, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000Z", + "reply": Object { + "parent": Object { + "cid": "cids(3)", + "uri": "record(4)", + }, + "root": Object { + "cid": "cids(3)", + "uri": "record(4)", + }, + }, + "text": "alice replies to dan", + }, + "replyCount": 0, + "repostCount": 0, + "uri": "record(0)", + "viewer": Object {}, + }, + "replies": Array [], + }, +} +`; + +exports[`pds views with blocking from block lists blocks thread reply 1`] = ` +Object { + "thread": Object { + "$type": "app.bsky.feed.defs#threadViewPost", + "post": Object { + "author": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(1)", + "muted": false, + }, + }, + "cid": "cids(0)", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "likeCount": 3, + "record": Object { + "$type": "app.bsky.feed.post", + "createdAt": "1970-01-01T00:00:00.000000Z", + "text": "again", + }, + "replyCount": 2, + "repostCount": 1, + "uri": "record(0)", + "viewer": Object { + "like": "record(4)", + "repost": "record(3)", + }, + }, + "replies": Array [ + Object { + "$type": "app.bsky.feed.defs#blockedPost", + "author": Object { + "did": "user(2)", + "viewer": Object { + "blockedBy": false, + "blocking": "record(6)", + }, + }, + "blocked": true, + "uri": "record(5)", + }, + Object { + "$type": "app.bsky.feed.defs#blockedPost", + "author": Object { + "did": "user(3)", + "viewer": Object { + "blockedBy": false, + "blocking": "record(6)", + }, + }, + "blocked": true, + "uri": "record(7)", + }, + ], + }, +} +`; + +exports[`pds views with blocking from block lists returns a users own list blocks 1`] = ` +Object { + "cursor": "0000000000000::bafycid", + "lists": Array [ + Object { + "cid": "cids(0)", + "creator": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "description": "its me!", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(2)", + "muted": false, + }, + }, + "description": "blah blah", + "indexedAt": "1970-01-01T00:00:00.000Z", + "name": "new list", + "purpose": "app.bsky.graph.defs#blocklist", + "uri": "record(0)", + "viewer": Object { + "blocked": "record(1)", + "muted": false, + }, + }, + Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "cid": "cids(3)", + "creator": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "description": "its me!", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(2)", + "muted": false, + }, + }, + "description": "big list of blocks", + "indexedAt": "1970-01-01T00:00:00.000Z", + "name": "alice blocks", + "purpose": "app.bsky.graph.defs#blocklist", + "uri": "record(4)", + "viewer": Object { + "blocked": "record(5)", + "muted": false, + }, + }, + ], +} +`; + +exports[`pds views with blocking from block lists returns lists associated with a user 1`] = ` +Object { + "cursor": "0000000000000::bafycid", + "lists": Array [ + Object { + "cid": "cids(0)", + "creator": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "description": "its me!", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(1)", + "muted": false, + }, + }, + "description": "blah blah", + "indexedAt": "1970-01-01T00:00:00.000Z", + "name": "new list", + "purpose": "app.bsky.graph.defs#blocklist", + "uri": "record(0)", + "viewer": Object { + "muted": false, + }, + }, + Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "cid": "cids(3)", + "creator": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "description": "its me!", + "did": "user(0)", + "displayName": "ali", + "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(1)", + "muted": false, + }, + }, + "description": "big list of blocks", + "indexedAt": "1970-01-01T00:00:00.000Z", + "name": "alice blocks", + "purpose": "app.bsky.graph.defs#blocklist", + "uri": "record(3)", + "viewer": Object { + "blocked": "record(4)", + "muted": false, + }, + }, + ], +} +`; + +exports[`pds views with blocking from block lists returns the contents of a list 1`] = ` +Object { + "cursor": "0000000000000::bafycid", + "items": Array [ + Object { + "subject": Object { + "did": "user(2)", + "handle": "carol.test", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "blocking": "record(0)", + "muted": false, + }, + }, + }, + Object { + "subject": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", + "description": "hi im bob label_me", + "did": "user(3)", + "displayName": "bobby", + "handle": "bob.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [], + "viewer": Object { + "blockedBy": false, + "blocking": "record(0)", + "following": "record(4)", + "muted": false, + }, + }, + }, + ], + "list": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(0)/cids(1)@jpeg", + "cid": "cids(0)", + "creator": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(0)/cids(1)@jpeg", + "description": "its me!", + "did": "user(1)", + "displayName": "ali", + "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(1)", + "uri": "record(3)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "followedBy": "record(2)", + "muted": false, + }, + }, + "description": "big list of blocks", + "indexedAt": "1970-01-01T00:00:00.000Z", + "name": "alice blocks", + "purpose": "app.bsky.graph.defs#blocklist", + "uri": "record(0)", + "viewer": Object { + "blocked": "record(1)", + "muted": false, + }, + }, +} +`; diff --git a/packages/bsky/tests/views/__snapshots__/follows.test.ts.snap b/packages/bsky/tests/views/__snapshots__/follows.test.ts.snap index 2061726d8e6..c85b0549de7 100644 --- a/packages/bsky/tests/views/__snapshots__/follows.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/follows.test.ts.snap @@ -5,9 +5,9 @@ Object { "cursor": "0000000000000::bafycid", "followers": Array [ Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-eve", - "did": "user(2)", + "did": "user(0)", "displayName": "display-eve", "handle": "eve.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -20,9 +20,9 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-dan", - "did": "user(4)", + "did": "user(2)", "displayName": "display-dan", "handle": "dan.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -35,9 +35,9 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "descript-bob", - "did": "user(6)", + "did": "user(4)", "displayName": "display-bob", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -50,9 +50,9 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(9)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(0)@jpeg", "description": "descript-carol", - "did": "user(8)", + "did": "user(6)", "displayName": "display-carol", "handle": "carol.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -66,9 +66,9 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(9)/cids(0)@jpeg", "description": "descript-alice", - "did": "user(0)", + "did": "user(8)", "displayName": "display-alice", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -86,24 +86,24 @@ Object { "cursor": "0000000000000::bafycid", "followers": Array [ Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-dan", - "did": "user(2)", + "did": "user(0)", "displayName": "display-dan", "handle": "dan.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(1)", + "following": "record(0)", "muted": false, }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-alice", - "did": "user(4)", + "did": "user(2)", "displayName": "display-alice", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -115,17 +115,17 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "descript-bob", - "did": "user(0)", + "did": "user(4)", "displayName": "display-bob", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", + "followedBy": "record(3)", + "following": "record(2)", "muted": false, }, }, @@ -137,39 +137,39 @@ Object { "cursor": "0000000000000::bafycid", "followers": Array [ Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-eve", - "did": "user(2)", + "did": "user(0)", "displayName": "display-eve", "handle": "eve.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(1)", + "following": "record(0)", "muted": false, }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-bob", - "did": "user(4)", + "did": "user(2)", "displayName": "display-bob", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(3)", + "following": "record(2)", "muted": false, }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "descript-alice", - "did": "user(6)", + "did": "user(4)", "displayName": "display-alice", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -181,17 +181,17 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(0)@jpeg", "description": "descript-carol", - "did": "user(0)", + "did": "user(6)", "displayName": "display-carol", "handle": "carol.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, @@ -203,9 +203,9 @@ Object { "cursor": "0000000000000::bafycid", "followers": Array [ Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-alice", - "did": "user(2)", + "did": "user(0)", "displayName": "display-alice", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -217,9 +217,9 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-dan", - "did": "user(0)", + "did": "user(2)", "displayName": "display-dan", "handle": "dan.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -239,24 +239,24 @@ Object { "cursor": "0000000000000::bafycid", "followers": Array [ Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-dan", - "did": "user(2)", + "did": "user(0)", "displayName": "display-dan", "handle": "dan.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(1)", + "following": "record(0)", "muted": false, }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-alice", - "did": "user(4)", + "did": "user(2)", "displayName": "display-alice", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -268,17 +268,17 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "descript-eve", - "did": "user(0)", + "did": "user(4)", "displayName": "display-eve", "handle": "eve.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", + "followedBy": "record(3)", + "following": "record(2)", "muted": false, }, }, @@ -290,9 +290,9 @@ Object { "cursor": "0000000000000::bafycid", "follows": Array [ Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-eve", - "did": "user(2)", + "did": "user(0)", "displayName": "display-eve", "handle": "eve.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -305,9 +305,9 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-dan", - "did": "user(4)", + "did": "user(2)", "displayName": "display-dan", "handle": "dan.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -320,9 +320,9 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "descript-carol", - "did": "user(6)", + "did": "user(4)", "displayName": "display-carol", "handle": "carol.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -335,9 +335,9 @@ Object { }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(9)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(0)@jpeg", "description": "descript-bob", - "did": "user(8)", + "did": "user(6)", "displayName": "display-bob", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -351,9 +351,9 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(9)/cids(0)@jpeg", "description": "descript-alice", - "did": "user(0)", + "did": "user(8)", "displayName": "display-alice", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -371,24 +371,24 @@ Object { "cursor": "0000000000000::bafycid", "follows": Array [ Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-carol", - "did": "user(2)", + "did": "user(0)", "displayName": "display-carol", "handle": "carol.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(1)", + "following": "record(0)", "muted": false, }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-alice", - "did": "user(4)", + "did": "user(2)", "displayName": "display-alice", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -400,17 +400,17 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "descript-bob", - "did": "user(0)", + "did": "user(4)", "displayName": "display-bob", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", + "followedBy": "record(3)", + "following": "record(2)", "muted": false, }, }, @@ -422,9 +422,9 @@ Object { "cursor": "0000000000000::bafycid", "follows": Array [ Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-alice", - "did": "user(2)", + "did": "user(0)", "displayName": "display-alice", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -436,9 +436,9 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-carol", - "did": "user(0)", + "did": "user(2)", "displayName": "display-carol", "handle": "carol.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -458,39 +458,39 @@ Object { "cursor": "0000000000000::bafycid", "follows": Array [ Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-eve", - "did": "user(2)", + "did": "user(0)", "displayName": "display-eve", "handle": "eve.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(1)", + "following": "record(0)", "muted": false, }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-bob", - "did": "user(4)", + "did": "user(2)", "displayName": "display-bob", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(3)", + "following": "record(2)", "muted": false, }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "descript-alice", - "did": "user(6)", + "did": "user(4)", "displayName": "display-alice", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -502,17 +502,17 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(7)/cids(0)@jpeg", "description": "descript-dan", - "did": "user(0)", + "did": "user(6)", "displayName": "display-dan", "handle": "dan.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, @@ -524,24 +524,24 @@ Object { "cursor": "0000000000000::bafycid", "follows": Array [ Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "descript-carol", - "did": "user(2)", + "did": "user(0)", "displayName": "display-carol", "handle": "carol.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(1)", + "following": "record(0)", "muted": false, }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(0)@jpeg", "description": "descript-alice", - "did": "user(4)", + "did": "user(2)", "displayName": "display-alice", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -553,17 +553,17 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(0)@jpeg", "description": "descript-eve", - "did": "user(0)", + "did": "user(4)", "displayName": "display-eve", "handle": "eve.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", + "followedBy": "record(3)", + "following": "record(2)", "muted": false, }, }, diff --git a/packages/bsky/tests/views/__snapshots__/likes.test.ts.snap b/packages/bsky/tests/views/__snapshots__/likes.test.ts.snap index 53f37486d1c..426467a3fa7 100644 --- a/packages/bsky/tests/views/__snapshots__/likes.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/likes.test.ts.snap @@ -24,7 +24,7 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(1)", + "following": "record(0)", "muted": false, }, }, @@ -38,8 +38,8 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(2)", + "following": "record(1)", "muted": false, }, }, @@ -57,8 +57,8 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, @@ -66,7 +66,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", }, ], - "uri": "record(0)", + "uri": "record(5)", } `; @@ -81,8 +81,8 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", + "followedBy": "record(1)", + "following": "record(0)", "muted": false, }, }, @@ -90,6 +90,6 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", }, ], - "uri": "record(0)", + "uri": "record(2)", } `; 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 748180f4d57..0a081f91292 100644 --- a/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap @@ -41,6 +41,24 @@ Object { "did": "user(0)", "displayName": "ali", "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(1)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -469,15 +487,15 @@ Object { "items": Array [ Object { "subject": Object { - "did": "user(0)", + "did": "user(2)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(1)", + "followedBy": "record(3)", "muted": true, "mutedByList": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(0)/cids(1)@jpeg", "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "name": "alice mutes", @@ -492,19 +510,19 @@ Object { }, Object { "subject": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(3)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", "description": "hi im bob label_me", - "did": "user(2)", + "did": "user(3)", "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(2)", + "following": "record(4)", "muted": true, "mutedByList": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(0)/cids(1)@jpeg", "cid": "cids(0)", "indexedAt": "1970-01-01T00:00:00.000Z", "name": "alice mutes", @@ -519,12 +537,12 @@ Object { }, ], "list": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(0)/cids(1)@jpeg", "cid": "cids(0)", "creator": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(0)/cids(1)@jpeg", "description": "its me!", - "did": "user(4)", + "did": "user(1)", "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -533,22 +551,22 @@ Object { "cid": "cids(2)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", - "uri": "record(4)", + "src": "user(1)", + "uri": "record(2)", "val": "self-label-a", }, Object { "cid": "cids(2)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(4)", - "uri": "record(4)", + "src": "user(1)", + "uri": "record(2)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", + "followedBy": "record(1)", "muted": false, }, }, diff --git a/packages/bsky/tests/views/block-lists.test.ts b/packages/bsky/tests/views/block-lists.test.ts new file mode 100644 index 00000000000..0a8a223e046 --- /dev/null +++ b/packages/bsky/tests/views/block-lists.test.ts @@ -0,0 +1,407 @@ +import AtpAgent from '@atproto/api' +import { TestNetwork } from '@atproto/dev-env' +import { forSnapshot } from '../_util' +import { SeedClient } from '../seeds/client' +import basicSeed from '../seeds/basic' +import { RecordRef } from '@atproto/bsky/tests/seeds/client' +import { BlockedActorError } from '@atproto/api/src/client/types/app/bsky/feed/getAuthorFeed' +import { BlockedByActorError } from '@atproto/api/src/client/types/app/bsky/feed/getAuthorFeed' + +describe('pds views with blocking from block lists', () => { + let network: TestNetwork + let agent: AtpAgent + let pdsAgent: AtpAgent + let sc: SeedClient + let aliceReplyToDan: { ref: RecordRef } + + let alice: string + let bob: string + let carol: string + let dan: string + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'views_block_lists', + }) + agent = network.bsky.getClient() + pdsAgent = network.pds.getClient() + sc = new SeedClient(pdsAgent) + await basicSeed(sc) + alice = sc.dids.alice + bob = sc.dids.bob + carol = sc.dids.carol + dan = sc.dids.dan + // add follows to ensure blocks work even w follows + await sc.follow(carol, dan) + await sc.follow(dan, carol) + aliceReplyToDan = await sc.reply( + alice, + sc.posts[dan][0].ref, + sc.posts[dan][0].ref, + 'alice replies to dan', + ) + await network.processAll() + }) + + afterAll(async () => { + await network.close() + }) + + let listUri: string + + it('creates a list with some items', async () => { + const avatar = await sc.uploadFile( + alice, + 'tests/image/fixtures/key-portrait-small.jpg', + 'image/jpeg', + ) + // alice creates block list with bob & carol that dan uses + const list = await pdsAgent.api.app.bsky.graph.list.create( + { repo: alice }, + { + name: 'alice blocks', + purpose: 'app.bsky.graph.defs#blocklist', + description: 'big list of blocks', + avatar: avatar.image, + createdAt: new Date().toISOString(), + }, + sc.getHeaders(alice), + ) + listUri = list.uri + await pdsAgent.api.app.bsky.graph.listitem.create( + { repo: alice }, + { + subject: sc.dids.bob, + list: list.uri, + createdAt: new Date().toISOString(), + }, + sc.getHeaders(alice), + ) + await pdsAgent.api.app.bsky.graph.listitem.create( + { repo: alice }, + { + subject: sc.dids.carol, + list: list.uri, + createdAt: new Date().toISOString(), + }, + sc.getHeaders(alice), + ) + await network.processAll() + }) + + it('uses a list for blocks', async () => { + await pdsAgent.api.app.bsky.graph.listblock.create( + { repo: dan }, + { + subject: listUri, + createdAt: new Date().toISOString(), + }, + sc.getHeaders(dan), + ) + await network.processAll() + }) + + it('blocks thread post', async () => { + const { carol, dan } = sc.dids + const { data: threadAlice } = await agent.api.app.bsky.feed.getPostThread( + { depth: 1, uri: sc.posts[carol][0].ref.uriStr }, + { headers: await network.serviceHeaders(dan) }, + ) + expect(threadAlice.thread).toEqual( + expect.objectContaining({ + $type: 'app.bsky.feed.defs#blockedPost', + uri: sc.posts[carol][0].ref.uriStr, + blocked: true, + }), + ) + const { data: threadCarol } = await agent.api.app.bsky.feed.getPostThread( + { depth: 1, uri: sc.posts[dan][0].ref.uriStr }, + { headers: await network.serviceHeaders(carol) }, + ) + expect(threadCarol.thread).toEqual( + expect.objectContaining({ + $type: 'app.bsky.feed.defs#blockedPost', + uri: sc.posts[dan][0].ref.uriStr, + blocked: true, + }), + ) + }) + + it('blocks thread reply', async () => { + // Contains reply by carol + const { data: thread } = await agent.api.app.bsky.feed.getPostThread( + { depth: 1, uri: sc.posts[alice][1].ref.uriStr }, + { headers: await network.serviceHeaders(dan) }, + ) + expect(forSnapshot(thread)).toMatchSnapshot() + }) + + it('blocks thread parent', async () => { + // Parent is a post by dan + const { data: thread } = await agent.api.app.bsky.feed.getPostThread( + { depth: 1, uri: aliceReplyToDan.ref.uriStr }, + { headers: await network.serviceHeaders(carol) }, + ) + expect(forSnapshot(thread)).toMatchSnapshot() + }) + + it('blocks record embeds', async () => { + // Contains a deep embed of carol's post, blocked by dan + const { data: thread } = await agent.api.app.bsky.feed.getPostThread( + { depth: 0, uri: sc.posts[alice][2].ref.uriStr }, + { headers: await network.serviceHeaders(dan) }, + ) + expect(forSnapshot(thread)).toMatchSnapshot() + }) + + it('errors on getting author feed', async () => { + const attempt1 = agent.api.app.bsky.feed.getAuthorFeed( + { actor: carol }, + { headers: await network.serviceHeaders(dan) }, + ) + await expect(attempt1).rejects.toThrow(BlockedActorError) + + const attempt2 = agent.api.app.bsky.feed.getAuthorFeed( + { actor: dan }, + { headers: await network.serviceHeaders(carol) }, + ) + await expect(attempt2).rejects.toThrow(BlockedByActorError) + }) + + it('strips blocked users out of getTimeline', async () => { + const resCarol = await agent.api.app.bsky.feed.getTimeline( + { limit: 100 }, + { headers: await network.serviceHeaders(carol) }, + ) + expect( + resCarol.data.feed.some((post) => post.post.author.did === dan), + ).toBeFalsy() + + const resDan = await agent.api.app.bsky.feed.getTimeline( + { limit: 100 }, + { headers: await network.serviceHeaders(dan) }, + ) + expect( + resDan.data.feed.some((post) => + [bob, carol].includes(post.post.author.did), + ), + ).toBeFalsy() + }) + + it('returns block status on getProfile', async () => { + const resCarol = await agent.api.app.bsky.actor.getProfile( + { actor: dan }, + { headers: await network.serviceHeaders(carol) }, + ) + expect(resCarol.data.viewer?.blocking).toBeUndefined() + expect(resCarol.data.viewer?.blockedBy).toBe(true) + + const resDan = await agent.api.app.bsky.actor.getProfile( + { actor: carol }, + { headers: await network.serviceHeaders(dan) }, + ) + expect(resDan.data.viewer?.blocking).toBeDefined() + expect(resDan.data.viewer?.blockedBy).toBe(false) + }) + + it('returns block status on getProfiles', async () => { + const resCarol = await agent.api.app.bsky.actor.getProfiles( + { actors: [alice, dan] }, + { headers: await network.serviceHeaders(carol) }, + ) + expect(resCarol.data.profiles[0].viewer?.blocking).toBeUndefined() + expect(resCarol.data.profiles[0].viewer?.blockedBy).toBe(false) + expect(resCarol.data.profiles[1].viewer?.blocking).toBeUndefined() + expect(resCarol.data.profiles[1].viewer?.blockedBy).toBe(true) + + const resDan = await agent.api.app.bsky.actor.getProfiles( + { actors: [alice, carol] }, + { headers: await network.serviceHeaders(dan) }, + ) + expect(resDan.data.profiles[0].viewer?.blocking).toBeUndefined() + expect(resDan.data.profiles[0].viewer?.blockedBy).toBe(false) + expect(resDan.data.profiles[1].viewer?.blocking).toBeDefined() + expect(resDan.data.profiles[1].viewer?.blockedBy).toBe(false) + }) + + it('does not return notifs for blocked accounts', async () => { + const resCarol = await agent.api.app.bsky.notification.listNotifications( + { + limit: 100, + }, + { headers: await network.serviceHeaders(carol) }, + ) + expect( + resCarol.data.notifications.some((notif) => notif.author.did === dan), + ).toBeFalsy() + + const resDan = await agent.api.app.bsky.notification.listNotifications( + { + limit: 100, + }, + { headers: await network.serviceHeaders(carol) }, + ) + expect( + resDan.data.notifications.some((notif) => notif.author.did === carol), + ).toBeFalsy() + }) + + it('does not return blocked accounts in actor search', async () => { + const resCarol = await agent.api.app.bsky.actor.searchActors( + { + term: 'dan.test', + }, + { headers: await network.serviceHeaders(carol) }, + ) + expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy() + + const resDan = await agent.api.app.bsky.actor.searchActors( + { + term: 'carol.test', + }, + { headers: await network.serviceHeaders(dan) }, + ) + expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy() + }) + + it('does not return blocked accounts in actor search typeahead', async () => { + const resCarol = await agent.api.app.bsky.actor.searchActorsTypeahead( + { + term: 'dan.test', + }, + { headers: await network.serviceHeaders(carol) }, + ) + expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy() + + const resDan = await agent.api.app.bsky.actor.searchActorsTypeahead( + { + term: 'carol.test', + }, + { headers: await network.serviceHeaders(dan) }, + ) + expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy() + }) + + it('does not return blocked accounts in get suggestions', async () => { + // unfollow so they _would_ show up in suggestions if not for block + await sc.unfollow(carol, dan) + await sc.unfollow(dan, carol) + await network.processAll() + + const resCarol = await agent.api.app.bsky.actor.getSuggestions( + { + limit: 100, + }, + { headers: await network.serviceHeaders(carol) }, + ) + expect(resCarol.data.actors.some((actor) => actor.did === dan)).toBeFalsy() + + const resDan = await agent.api.app.bsky.actor.getSuggestions( + { + limit: 100, + }, + { headers: await network.serviceHeaders(dan) }, + ) + expect(resDan.data.actors.some((actor) => actor.did === carol)).toBeFalsy() + }) + + it('returns the contents of a list', async () => { + const res = await agent.api.app.bsky.graph.getList( + { list: listUri }, + { headers: await network.serviceHeaders(dan) }, + ) + expect(forSnapshot(res.data)).toMatchSnapshot() + }) + + it('paginates getList', async () => { + const full = await agent.api.app.bsky.graph.getList( + { list: listUri }, + { headers: await network.serviceHeaders(dan) }, + ) + const first = await agent.api.app.bsky.graph.getList( + { list: listUri, limit: 1 }, + { headers: await network.serviceHeaders(dan) }, + ) + const second = await agent.api.app.bsky.graph.getList( + { list: listUri, cursor: first.data.cursor }, + { headers: await network.serviceHeaders(dan) }, + ) + const combined = [...first.data.items, ...second.data.items] + expect(combined).toEqual(full.data.items) + }) + + let otherListUri: string + + it('returns lists associated with a user', async () => { + const listRes = await pdsAgent.api.app.bsky.graph.list.create( + { repo: alice }, + { + name: 'new list', + purpose: 'app.bsky.graph.defs#blocklist', + description: 'blah blah', + createdAt: new Date().toISOString(), + }, + sc.getHeaders(alice), + ) + otherListUri = listRes.uri + await network.processAll() + + const res = await agent.api.app.bsky.graph.getLists( + { actor: alice }, + { headers: await network.serviceHeaders(dan) }, + ) + expect(forSnapshot(res.data)).toMatchSnapshot() + }) + + it('paginates getLists', async () => { + const full = await agent.api.app.bsky.graph.getLists( + { actor: alice }, + { headers: await network.serviceHeaders(dan) }, + ) + const first = await agent.api.app.bsky.graph.getLists( + { actor: alice, limit: 1 }, + { headers: await network.serviceHeaders(dan) }, + ) + const second = await agent.api.app.bsky.graph.getLists( + { actor: alice, cursor: first.data.cursor }, + { headers: await network.serviceHeaders(dan) }, + ) + const combined = [...first.data.lists, ...second.data.lists] + expect(combined).toEqual(full.data.lists) + }) + + it('returns a users own list blocks', async () => { + await pdsAgent.api.app.bsky.graph.listblock.create( + { repo: dan }, + { + subject: otherListUri, + createdAt: new Date().toISOString(), + }, + sc.getHeaders(dan), + ) + await network.processAll() + + const res = await agent.api.app.bsky.graph.getListBlocks( + {}, + { headers: await network.serviceHeaders(dan) }, + ) + expect(forSnapshot(res.data)).toMatchSnapshot() + }) + + it('paginates getListBlocks', async () => { + const full = await agent.api.app.bsky.graph.getListBlocks( + {}, + { headers: await network.serviceHeaders(dan) }, + ) + const first = await agent.api.app.bsky.graph.getListBlocks( + { limit: 1 }, + { headers: await network.serviceHeaders(dan) }, + ) + const second = await agent.api.app.bsky.graph.getListBlocks( + { cursor: first.data.cursor }, + { headers: await network.serviceHeaders(dan) }, + ) + const combined = [...first.data.lists, ...second.data.lists] + expect(combined).toEqual(full.data.lists) + }) +}) diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getListBlocks.ts b/packages/pds/src/app-view/api/app/bsky/graph/getListBlocks.ts new file mode 100644 index 00000000000..98c55e14bc9 --- /dev/null +++ b/packages/pds/src/app-view/api/app/bsky/graph/getListBlocks.ts @@ -0,0 +1,19 @@ +import { Server } from '../../../../../lexicon' +import AppContext from '../../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.graph.getListBlocks({ + auth: ctx.accessVerifier, + handler: async ({ auth, params }) => { + const requester = auth.credentials.did + const res = await ctx.appviewAgent.api.app.bsky.graph.getListBlocks( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/app-view/api/app/bsky/index.ts b/packages/pds/src/app-view/api/app/bsky/index.ts index fc8c6baeace..7f9c458ae70 100644 --- a/packages/pds/src/app-view/api/app/bsky/index.ts +++ b/packages/pds/src/app-view/api/app/bsky/index.ts @@ -19,6 +19,7 @@ import getBlocks from './graph/getBlocks' import getFollowers from './graph/getFollowers' import getFollows from './graph/getFollows' import getList from './graph/getList' +import getListBlocks from './graph/getListBlocks' import getListMutes from './graph/getListMutes' import getLists from './graph/getLists' import getMutes from './graph/getMutes' @@ -55,6 +56,7 @@ export default function (server: Server, ctx: AppContext) { getFollowers(server, ctx) getFollows(server, ctx) getList(server, ctx) + getListBlocks(server, ctx) getListMutes(server, ctx) getLists(server, ctx) getMutes(server, ctx) diff --git a/packages/pds/src/app-view/services/indexing/index.ts b/packages/pds/src/app-view/services/indexing/index.ts index cdb27344251..346c31e83ea 100644 --- a/packages/pds/src/app-view/services/indexing/index.ts +++ b/packages/pds/src/app-view/services/indexing/index.ts @@ -1,7 +1,10 @@ import { CID } from 'multiformats/cid' import { WriteOpAction } from '@atproto/repo' import { AtUri } from '@atproto/syntax' +import { ids } from '../../../lexicon/lexicons' import Database from '../../../db' +import { BackgroundQueue } from '../../../event-stream/background-queue' +import { NoopProcessor } from './processor' import * as Post from './plugins/post' import * as Like from './plugins/like' import * as Repost from './plugins/repost' @@ -11,7 +14,6 @@ import * as List from './plugins/list' import * as ListItem from './plugins/list-item' import * as Profile from './plugins/profile' import * as FeedGenerator from './plugins/feed-generator' -import { BackgroundQueue } from '../../../event-stream/background-queue' export class IndexingService { records: { @@ -22,6 +24,7 @@ export class IndexingService { block: Block.PluginType list: List.PluginType listItem: ListItem.PluginType + listBlock: NoopProcessor profile: Profile.PluginType feedGenerator: FeedGenerator.PluginType } @@ -35,6 +38,11 @@ export class IndexingService { block: Block.makePlugin(this.db, backgroundQueue), list: List.makePlugin(this.db, backgroundQueue), listItem: ListItem.makePlugin(this.db, backgroundQueue), + listBlock: new NoopProcessor( + ids.AppBskyGraphListblock, + this.db, + backgroundQueue, + ), profile: Profile.makePlugin(this.db, backgroundQueue), feedGenerator: FeedGenerator.makePlugin(this.db, backgroundQueue), } diff --git a/packages/pds/src/app-view/services/indexing/processor.ts b/packages/pds/src/app-view/services/indexing/processor.ts index 8c93fccfb3f..6b6712889f2 100644 --- a/packages/pds/src/app-view/services/indexing/processor.ts +++ b/packages/pds/src/app-view/services/indexing/processor.ts @@ -240,3 +240,20 @@ export class RecordProcessor { } export default RecordProcessor + +export class NoopProcessor extends RecordProcessor { + constructor( + lexId: string, + appDb: Database, + backgroundQueue: BackgroundQueue, + ) { + super(appDb, backgroundQueue, { + lexId, + insertFn: async () => null, + deleteFn: async () => null, + findDuplicate: async () => null, + notifsForInsert: () => [], + notifsForDelete: () => ({ notifs: [], toDelete: [] }), + }) + } +} diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index a99d4d6e51b..df15a497c63 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -93,6 +93,7 @@ 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' @@ -1228,6 +1229,17 @@ export class GraphNS { 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, diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 193b9c39d37..c49b098002b 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -5728,6 +5728,10 @@ export const schemaDict = { muted: { type: 'boolean', }, + blocked: { + type: 'string', + format: 'at-uri', + }, }, }, }, @@ -5956,6 +5960,49 @@ export const schemaDict = { }, }, }, + AppBskyGraphGetListBlocks: { + lexicon: 1, + id: 'app.bsky.graph.getListBlocks', + defs: { + main: { + type: 'query', + description: "Which lists is the requester's account 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', @@ -6141,6 +6188,31 @@ export const schemaDict = { }, }, }, + 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', @@ -6798,10 +6870,12 @@ export const ids = { 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', 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', diff --git a/packages/pds/src/lexicon/types/app/bsky/graph/defs.ts b/packages/pds/src/lexicon/types/app/bsky/graph/defs.ts index e50338d488d..63c05b5faa3 100644 --- a/packages/pds/src/lexicon/types/app/bsky/graph/defs.ts +++ b/packages/pds/src/lexicon/types/app/bsky/graph/defs.ts @@ -81,6 +81,7 @@ export const MODLIST = 'app.bsky.graph.defs#modlist' export interface ListViewerState { muted?: boolean + blocked?: string [k: string]: unknown } diff --git a/packages/pds/src/lexicon/types/app/bsky/graph/getListBlocks.ts b/packages/pds/src/lexicon/types/app/bsky/graph/getListBlocks.ts new file mode 100644 index 00000000000..04cca70b44d --- /dev/null +++ b/packages/pds/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/pds/src/lexicon/types/app/bsky/graph/listblock.ts b/packages/pds/src/lexicon/types/app/bsky/graph/listblock.ts new file mode 100644 index 00000000000..59f2e057eb5 --- /dev/null +++ b/packages/pds/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/pds/tests/proxied/__snapshots__/views.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap index 6a055da07a2..1e7b75c9cc8 100644 --- a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap @@ -117,9 +117,9 @@ exports[`proxies view requests actor.getSuggestions 1`] = ` Object { "actors": Array [ Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", "description": "hi im bob label_me", - "did": "user(1)", + "did": "user(0)", "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", @@ -128,7 +128,7 @@ Object { "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(1)", + "src": "user(0)", "uri": "record(1)", "val": "self-label-a", }, @@ -136,7 +136,7 @@ Object { "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(1)", + "src": "user(0)", "uri": "record(1)", "val": "self-label-b", }, @@ -148,14 +148,14 @@ Object { }, }, Object { - "did": "user(0)", + "did": "user(2)", "handle": "dan.test", "labels": Array [ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "user(0)", + "uri": "user(2)", "val": "repo-action-label", }, ], @@ -165,7 +165,7 @@ Object { }, }, ], - "cursor": "user(0)", + "cursor": "user(2)", } `; @@ -713,7 +713,7 @@ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(0)", - "uri": "record(3)", + "uri": "record(2)", "val": "self-label-a", }, Object { @@ -721,14 +721,14 @@ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(0)", - "uri": "record(3)", + "uri": "record(2)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", + "followedBy": "record(1)", + "following": "record(0)", "muted": false, }, }, @@ -749,7 +749,7 @@ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(4)", + "uri": "record(3)", "val": "self-label-a", }, Object { @@ -757,7 +757,7 @@ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "user(2)", - "uri": "record(4)", + "uri": "record(3)", "val": "self-label-b", }, ], @@ -770,7 +770,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", }, ], - "uri": "record(0)", + "uri": "record(4)", } `; @@ -971,13 +971,13 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(2)", - "following": "record(1)", + "followedBy": "record(1)", + "following": "record(0)", "muted": false, }, }, ], - "uri": "record(0)", + "uri": "record(2)", } `; @@ -2225,45 +2225,45 @@ Object { "cursor": "0000000000000::bafycid", "followers": Array [ Object { - "did": "user(2)", + "did": "user(0)", "handle": "dan.test", "labels": Array [ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "user(2)", + "uri": "user(0)", "val": "repo-action-label", }, ], "viewer": Object { "blockedBy": false, - "following": "record(3)", + "following": "record(0)", "muted": false, }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(0)@jpeg", "description": "its me!", - "did": "user(3)", + "did": "user(1)", "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(4)", + "src": "user(1)", + "uri": "record(1)", "val": "self-label-a", }, Object { - "cid": "cids(2)", + "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(4)", + "src": "user(1)", + "uri": "record(1)", "val": "self-label-b", }, ], @@ -2274,34 +2274,34 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(0)@jpeg", "description": "hi im bob label_me", - "did": "user(0)", + "did": "user(3)", "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(1)", + "cid": "cids(2)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(0)", - "uri": "record(2)", + "src": "user(3)", + "uri": "record(4)", "val": "self-label-a", }, Object { - "cid": "cids(1)", + "cid": "cids(2)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(0)", - "uri": "record(2)", + "src": "user(3)", + "uri": "record(4)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", + "followedBy": "record(3)", + "following": "record(2)", "muted": false, }, }, @@ -2313,38 +2313,38 @@ Object { "cursor": "0000000000000::bafycid", "follows": Array [ Object { - "did": "user(2)", + "did": "user(0)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(4)", - "following": "record(3)", + "followedBy": "record(1)", + "following": "record(0)", "muted": false, }, }, Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(0)@jpeg", "description": "its me!", - "did": "user(3)", + "did": "user(1)", "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(2)", + "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(5)", + "src": "user(1)", + "uri": "record(2)", "val": "self-label-a", }, Object { - "cid": "cids(2)", + "cid": "cids(1)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(5)", + "src": "user(1)", + "uri": "record(2)", "val": "self-label-b", }, ], @@ -2355,34 +2355,34 @@ Object { }, ], "subject": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(0)@jpeg", "description": "hi im bob label_me", - "did": "user(0)", + "did": "user(3)", "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(1)", + "cid": "cids(2)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(0)", - "uri": "record(2)", + "src": "user(3)", + "uri": "record(5)", "val": "self-label-a", }, Object { - "cid": "cids(1)", + "cid": "cids(2)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(0)", - "uri": "record(2)", + "src": "user(3)", + "uri": "record(5)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, @@ -2395,40 +2395,40 @@ Object { "items": Array [ Object { "subject": Object { - "did": "user(0)", + "did": "user(2)", "handle": "carol.test", "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", + "followedBy": "record(5)", + "following": "record(4)", "muted": false, }, }, }, Object { "subject": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", "description": "its me!", - "did": "user(1)", + "did": "user(3)", "displayName": "ali", "handle": "alice.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(1)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(1)", - "uri": "record(2)", + "src": "user(3)", + "uri": "record(6)", "val": "self-label-a", }, Object { - "cid": "cids(1)", + "cid": "cids(3)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(1)", - "uri": "record(2)", + "src": "user(3)", + "uri": "record(6)", "val": "self-label-b", }, ], @@ -2440,36 +2440,36 @@ Object { }, ], "list": Object { - "cid": "cids(2)", + "cid": "cids(0)", "creator": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(0)@jpeg", + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", "description": "hi im bob label_me", - "did": "user(3)", + "did": "user(0)", "displayName": "bobby", "handle": "bob.test", "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { - "cid": "cids(3)", + "cid": "cids(2)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(6)", + "src": "user(0)", + "uri": "record(3)", "val": "self-label-a", }, Object { - "cid": "cids(3)", + "cid": "cids(2)", "cts": "1970-01-01T00:00:00.000Z", "neg": false, - "src": "user(3)", - "uri": "record(6)", + "src": "user(0)", + "uri": "record(3)", "val": "self-label-b", }, ], "viewer": Object { "blockedBy": false, - "followedBy": "record(5)", - "following": "record(4)", + "followedBy": "record(2)", + "following": "record(1)", "muted": false, }, }, @@ -2477,7 +2477,7 @@ Object { "indexedAt": "1970-01-01T00:00:00.000Z", "name": "bob mutes", "purpose": "app.bsky.graph.defs#modlist", - "uri": "record(3)", + "uri": "record(0)", "viewer": Object { "muted": false, }, @@ -2485,6 +2485,56 @@ Object { } `; +exports[`proxies view requests graph.getListBlocks 1`] = ` +Object { + "cursor": "0000000000000::bafycid", + "lists": Array [ + Object { + "cid": "cids(0)", + "creator": Object { + "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "description": "hi im bob label_me", + "did": "user(0)", + "displayName": "bobby", + "handle": "bob.test", + "indexedAt": "1970-01-01T00:00:00.000Z", + "labels": Array [ + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-a", + }, + Object { + "cid": "cids(2)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(2)", + "val": "self-label-b", + }, + ], + "viewer": Object { + "blockedBy": false, + "muted": false, + }, + }, + "description": "bob's list of mutes", + "indexedAt": "1970-01-01T00:00:00.000Z", + "name": "bob mutes", + "purpose": "app.bsky.graph.defs#modlist", + "uri": "record(0)", + "viewer": Object { + "blocked": "record(1)", + "muted": false, + }, + }, + ], +} +`; + exports[`proxies view requests graph.getLists 1`] = ` Object { "cursor": "0000000000000::bafycid", diff --git a/packages/pds/tests/proxied/views.test.ts b/packages/pds/tests/proxied/views.test.ts index 066cc780059..84079af1c44 100644 --- a/packages/pds/tests/proxied/views.test.ts +++ b/packages/pds/tests/proxied/views.test.ts @@ -524,4 +524,27 @@ describe('proxies view requests', () => { ) expect([...pt1.data.lists, ...pt2.data.lists]).toEqual(res.data.lists) }) + + it('graph.getListBlocks', async () => { + await agent.api.app.bsky.graph.listblock.create( + { repo: bob }, + { + subject: listUri, + createdAt: new Date().toISOString(), + }, + sc.getHeaders(bob), + ) + await network.processAll() + const pt1 = await agent.api.app.bsky.graph.getListBlocks( + {}, + { headers: sc.getHeaders(bob) }, + ) + expect(forSnapshot(pt1.data)).toMatchSnapshot() + const pt2 = await agent.api.app.bsky.graph.getListBlocks( + { cursor: pt1.data.cursor }, + { headers: sc.getHeaders(bob) }, + ) + expect(pt2.data.lists).toEqual([]) + expect(pt2.data.cursor).not.toBeDefined() + }) }) From 1053c6f26c8fcfd81e7abb84a852b39799a46eb3 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 12 Sep 2023 12:37:36 -0500 Subject: [PATCH 232/237] Prevent user pref races (#1576) prevent user pref races --- packages/pds/src/services/account/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/pds/src/services/account/index.ts b/packages/pds/src/services/account/index.ts index dd28af01df5..8173d9160af 100644 --- a/packages/pds/src/services/account/index.ts +++ b/packages/pds/src/services/account/index.ts @@ -590,6 +590,15 @@ export class AccountService { `Some preferences are not in the ${namespace} namespace`, ) } + // short-held row lock to prevent races + if (this.db.dialect === 'pg') { + await this.db.db + .selectFrom('user_account') + .selectAll() + .forUpdate() + .where('did', '=', did) + .executeTakeFirst() + } // get all current prefs for user and prep new pref rows const allPrefs = await this.db.db .selectFrom('user_pref') From 3cdc72a0a7f974129fcd781fce26254e6a623452 Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 12 Sep 2023 12:37:45 -0500 Subject: [PATCH 233/237] Fix getRepo `since` (#1579) * fix schemas * test * format --- lexicons/com/atproto/sync/getRepo.json | 1 - lexicons/com/atproto/sync/listBlobs.json | 1 - packages/api/src/client/lexicons.ts | 2 -- packages/bsky/src/lexicon/lexicons.ts | 2 -- packages/pds/src/lexicon/lexicons.ts | 2 -- packages/pds/tests/sync/sync.test.ts | 33 ++++++++++++++++++++++++ 6 files changed, 33 insertions(+), 8 deletions(-) diff --git a/lexicons/com/atproto/sync/getRepo.json b/lexicons/com/atproto/sync/getRepo.json index 87ecaa1f796..6bb6de2b1e4 100644 --- a/lexicons/com/atproto/sync/getRepo.json +++ b/lexicons/com/atproto/sync/getRepo.json @@ -16,7 +16,6 @@ }, "since": { "type": "string", - "format": "cid", "description": "The revision of the repo to catch up from." } } diff --git a/lexicons/com/atproto/sync/listBlobs.json b/lexicons/com/atproto/sync/listBlobs.json index 01e01391256..9f25b1330a6 100644 --- a/lexicons/com/atproto/sync/listBlobs.json +++ b/lexicons/com/atproto/sync/listBlobs.json @@ -16,7 +16,6 @@ }, "since": { "type": "string", - "format": "cid", "description": "Optional revision of the repo to list blobs since" }, "limit": { diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index c49b098002b..6f1cef89925 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -3149,7 +3149,6 @@ export const schemaDict = { }, since: { type: 'string', - format: 'cid', description: 'The revision of the repo to catch up from.', }, }, @@ -3178,7 +3177,6 @@ export const schemaDict = { }, since: { type: 'string', - format: 'cid', description: 'Optional revision of the repo to list blobs since', }, limit: { diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index c49b098002b..6f1cef89925 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -3149,7 +3149,6 @@ export const schemaDict = { }, since: { type: 'string', - format: 'cid', description: 'The revision of the repo to catch up from.', }, }, @@ -3178,7 +3177,6 @@ export const schemaDict = { }, since: { type: 'string', - format: 'cid', description: 'Optional revision of the repo to list blobs since', }, limit: { diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index c49b098002b..6f1cef89925 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -3149,7 +3149,6 @@ export const schemaDict = { }, since: { type: 'string', - format: 'cid', description: 'The revision of the repo to catch up from.', }, }, @@ -3178,7 +3177,6 @@ export const schemaDict = { }, since: { type: 'string', - format: 'cid', description: 'Optional revision of the repo to list blobs since', }, limit: { diff --git a/packages/pds/tests/sync/sync.test.ts b/packages/pds/tests/sync/sync.test.ts index 73947eddbc3..27311cbf81d 100644 --- a/packages/pds/tests/sync/sync.test.ts +++ b/packages/pds/tests/sync/sync.test.ts @@ -118,6 +118,39 @@ describe('repo sync', () => { expect(commit.data.cid).toEqual(currRoot?.toString()) }) + it('syncs `since` a given rev', async () => { + const repoBefore = await repo.Repo.load(storage, currRoot) + + // add a post + const { obj, uri } = await makePost(sc, did) + if (!repoData[uri.collection]) { + repoData[uri.collection] = {} + } + repoData[uri.collection][uri.rkey] = obj + uris.push(uri) + + const carRes = await agent.api.com.atproto.sync.getRepo({ + did, + since: repoBefore.commit.rev, + }) + const car = await repo.readCarWithRoot(carRes.data) + expect(car.blocks.size).toBeLessThan(10) // should only contain new blocks + const synced = await repo.verifyDiff( + repoBefore, + car.blocks, + car.root, + did, + ctx.repoSigningKey.did(), + ) + expect(synced.writes.length).toBe(1) + await storage.applyCommit(synced.commit) + const loaded = await repo.Repo.load(storage, car.root) + const contents = await loaded.getContents() + expect(contents).toEqual(repoData) + + currRoot = car.root + }) + it('sync a record proof', async () => { const collection = Object.keys(repoData)[0] const rkey = Object.keys(repoData[collection])[0] From 07bb0da0cf83cc045372bf8f592255a872798d4f Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 12 Sep 2023 12:37:57 -0500 Subject: [PATCH 234/237] Remove temp.upgradeRepoVersion (#1588) remove temp.upgradeRepoVersion --- .../com/atproto/temp/upgradeRepoVersion.json | 21 --- packages/api/src/client/index.ts | 23 --- packages/api/src/client/lexicons.ts | 27 --- .../com/atproto/temp/upgradeRepoVersion.ts | 33 ---- packages/bsky/src/lexicon/index.ts | 22 --- packages/bsky/src/lexicon/lexicons.ts | 27 --- .../com/atproto/temp/upgradeRepoVersion.ts | 39 ---- packages/pds/src/api/com/atproto/index.ts | 2 - .../src/api/com/atproto/upgradeRepoVersion.ts | 140 -------------- packages/pds/src/lexicon/index.ts | 22 --- packages/pds/src/lexicon/lexicons.ts | 27 --- .../com/atproto/temp/upgradeRepoVersion.ts | 39 ---- .../migrations/repo-version-upgrade.test.ts | 173 ------------------ 13 files changed, 595 deletions(-) delete mode 100644 lexicons/com/atproto/temp/upgradeRepoVersion.json delete mode 100644 packages/api/src/client/types/com/atproto/temp/upgradeRepoVersion.ts delete mode 100644 packages/bsky/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts delete mode 100644 packages/pds/src/api/com/atproto/upgradeRepoVersion.ts delete mode 100644 packages/pds/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts delete mode 100644 packages/pds/tests/migrations/repo-version-upgrade.test.ts diff --git a/lexicons/com/atproto/temp/upgradeRepoVersion.json b/lexicons/com/atproto/temp/upgradeRepoVersion.json deleted file mode 100644 index 05d8c7197fd..00000000000 --- a/lexicons/com/atproto/temp/upgradeRepoVersion.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "lexicon": 1, - "id": "com.atproto.temp.upgradeRepoVersion", - "defs": { - "main": { - "type": "procedure", - "description": "Upgrade a repo to v3", - "input": { - "encoding": "application/json", - "schema": { - "type": "object", - "required": ["did"], - "properties": { - "did": { "type": "string", "format": "did" }, - "force": { "type": "boolean" } - } - } - } - } - } -} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index f2921d3e5a5..16728348374 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -70,7 +70,6 @@ import * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos' import * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate' import * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl' import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos' -import * as ComAtprotoTempUpgradeRepoVersion from './types/com/atproto/temp/upgradeRepoVersion' import * as AppBskyActorDefs from './types/app/bsky/actor/defs' import * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile' @@ -194,7 +193,6 @@ export * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos' export * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate' export * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl' export * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos' -export * as ComAtprotoTempUpgradeRepoVersion from './types/com/atproto/temp/upgradeRepoVersion' export * as AppBskyActorDefs from './types/app/bsky/actor/defs' export * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' export * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile' @@ -322,7 +320,6 @@ export class AtprotoNS { repo: RepoNS server: ServerNS sync: SyncNS - temp: TempNS constructor(service: AtpServiceClient) { this._service = service @@ -333,7 +330,6 @@ export class AtprotoNS { this.repo = new RepoNS(service) this.server = new ServerNS(service) this.sync = new SyncNS(service) - this.temp = new TempNS(service) } } @@ -1009,25 +1005,6 @@ export class SyncNS { } } -export class TempNS { - _service: AtpServiceClient - - constructor(service: AtpServiceClient) { - this._service = service - } - - upgradeRepoVersion( - data?: ComAtprotoTempUpgradeRepoVersion.InputSchema, - opts?: ComAtprotoTempUpgradeRepoVersion.CallOptions, - ): Promise { - return this._service.xrpc - .call('com.atproto.temp.upgradeRepoVersion', opts?.qp, data, opts) - .catch((e) => { - throw ComAtprotoTempUpgradeRepoVersion.toKnownErr(e) - }) - } -} - export class AppNS { _service: AtpServiceClient bsky: BskyNS diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 6f1cef89925..30da3464cb2 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -3517,32 +3517,6 @@ export const schemaDict = { }, }, }, - ComAtprotoTempUpgradeRepoVersion: { - lexicon: 1, - id: 'com.atproto.temp.upgradeRepoVersion', - defs: { - main: { - type: 'procedure', - description: 'Upgrade a repo to v3', - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['did'], - properties: { - did: { - type: 'string', - format: 'did', - }, - force: { - type: 'boolean', - }, - }, - }, - }, - }, - }, - }, AppBskyActorDefs: { lexicon: 1, id: 'app.bsky.actor.defs', @@ -6828,7 +6802,6 @@ export const ids = { ComAtprotoSyncNotifyOfUpdate: 'com.atproto.sync.notifyOfUpdate', ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl', ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos', - ComAtprotoTempUpgradeRepoVersion: 'com.atproto.temp.upgradeRepoVersion', AppBskyActorDefs: 'app.bsky.actor.defs', AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences', AppBskyActorGetProfile: 'app.bsky.actor.getProfile', diff --git a/packages/api/src/client/types/com/atproto/temp/upgradeRepoVersion.ts b/packages/api/src/client/types/com/atproto/temp/upgradeRepoVersion.ts deleted file mode 100644 index abaf3a9f1b0..00000000000 --- a/packages/api/src/client/types/com/atproto/temp/upgradeRepoVersion.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * GENERATED CODE - DO NOT MODIFY - */ -import { Headers, XRPCError } from '@atproto/xrpc' -import { ValidationResult, BlobRef } from '@atproto/lexicon' -import { isObj, hasProp } from '../../../../util' -import { lexicons } from '../../../../lexicons' -import { CID } from 'multiformats/cid' - -export interface QueryParams {} - -export interface InputSchema { - did: string - force?: boolean - [k: string]: unknown -} - -export interface CallOptions { - headers?: Headers - qp?: QueryParams - encoding: 'application/json' -} - -export interface Response { - success: boolean - headers: Headers -} - -export function toKnownErr(e: any) { - if (e instanceof XRPCError) { - } - return e -} diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index df15a497c63..028b3cbf397 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -67,7 +67,6 @@ import * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos' import * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate' import * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl' import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos' -import * as ComAtprotoTempUpgradeRepoVersion from './types/com/atproto/temp/upgradeRepoVersion' import * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile' import * as AppBskyActorGetProfiles from './types/app/bsky/actor/getProfiles' @@ -163,7 +162,6 @@ export class AtprotoNS { repo: RepoNS server: ServerNS sync: SyncNS - temp: TempNS constructor(server: Server) { this._server = server @@ -174,7 +172,6 @@ export class AtprotoNS { this.repo = new RepoNS(server) this.server = new ServerNS(server) this.sync = new SyncNS(server) - this.temp = new TempNS(server) } } @@ -872,25 +869,6 @@ export class SyncNS { } } -export class TempNS { - _server: Server - - constructor(server: Server) { - this._server = server - } - - upgradeRepoVersion( - cfg: ConfigOf< - AV, - ComAtprotoTempUpgradeRepoVersion.Handler>, - ComAtprotoTempUpgradeRepoVersion.HandlerReqCtx> - >, - ) { - const nsid = 'com.atproto.temp.upgradeRepoVersion' // @ts-ignore - return this._server.xrpc.method(nsid, cfg) - } -} - export class AppNS { _server: Server bsky: BskyNS diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 6f1cef89925..30da3464cb2 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -3517,32 +3517,6 @@ export const schemaDict = { }, }, }, - ComAtprotoTempUpgradeRepoVersion: { - lexicon: 1, - id: 'com.atproto.temp.upgradeRepoVersion', - defs: { - main: { - type: 'procedure', - description: 'Upgrade a repo to v3', - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['did'], - properties: { - did: { - type: 'string', - format: 'did', - }, - force: { - type: 'boolean', - }, - }, - }, - }, - }, - }, - }, AppBskyActorDefs: { lexicon: 1, id: 'app.bsky.actor.defs', @@ -6828,7 +6802,6 @@ export const ids = { ComAtprotoSyncNotifyOfUpdate: 'com.atproto.sync.notifyOfUpdate', ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl', ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos', - ComAtprotoTempUpgradeRepoVersion: 'com.atproto.temp.upgradeRepoVersion', AppBskyActorDefs: 'app.bsky.actor.defs', AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences', AppBskyActorGetProfile: 'app.bsky.actor.getProfile', diff --git a/packages/bsky/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts b/packages/bsky/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts deleted file mode 100644 index c5b77876fec..00000000000 --- a/packages/bsky/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * GENERATED CODE - DO NOT MODIFY - */ -import express from 'express' -import { ValidationResult, BlobRef } from '@atproto/lexicon' -import { lexicons } from '../../../../lexicons' -import { isObj, hasProp } from '../../../../util' -import { CID } from 'multiformats/cid' -import { HandlerAuth } from '@atproto/xrpc-server' - -export interface QueryParams {} - -export interface InputSchema { - did: string - force?: boolean - [k: string]: unknown -} - -export interface HandlerInput { - encoding: 'application/json' - body: InputSchema -} - -export interface HandlerError { - status: number - message?: string -} - -export type HandlerOutput = HandlerError | void -export type HandlerReqCtx = { - auth: HA - params: QueryParams - input: HandlerInput - req: express.Request - res: express.Response -} -export type Handler = ( - ctx: HandlerReqCtx, -) => Promise | HandlerOutput diff --git a/packages/pds/src/api/com/atproto/index.ts b/packages/pds/src/api/com/atproto/index.ts index 59fca0fd5a9..a5c26c80495 100644 --- a/packages/pds/src/api/com/atproto/index.ts +++ b/packages/pds/src/api/com/atproto/index.ts @@ -6,7 +6,6 @@ import moderation from './moderation' import repo from './repo' import serverMethods from './server' import sync from './sync' -import upgradeRepoVersion from './upgradeRepoVersion' export default function (server: Server, ctx: AppContext) { admin(server, ctx) @@ -15,5 +14,4 @@ export default function (server: Server, ctx: AppContext) { repo(server, ctx) serverMethods(server, ctx) sync(server, ctx) - upgradeRepoVersion(server, ctx) } diff --git a/packages/pds/src/api/com/atproto/upgradeRepoVersion.ts b/packages/pds/src/api/com/atproto/upgradeRepoVersion.ts deleted file mode 100644 index dd20943be3e..00000000000 --- a/packages/pds/src/api/com/atproto/upgradeRepoVersion.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' -import { TID, chunkArray, wait } from '@atproto/common' -import { Server } from '../../../lexicon' -import SqlRepoStorage from '../../../sql-repo-storage' -import AppContext from '../../../context' -import { - BlockMap, - CidSet, - DataDiff, - MST, - MemoryBlockstore, - def, - signCommit, -} from '@atproto/repo' -import { CID } from 'multiformats/cid' -import { formatSeqCommit, sequenceEvt } from '../../../sequencer' -import { httpLogger as log } from '../../../logger' - -export default function (server: Server, ctx: AppContext) { - server.com.atproto.temp.upgradeRepoVersion({ - auth: ctx.roleVerifier, - handler: async ({ input, auth }) => { - if (!auth.credentials.admin) { - throw new InvalidRequestError('must be admin') - } - const { did, force } = input.body - - await ctx.db.transaction(async (dbTxn) => { - const storage = new SqlRepoStorage(dbTxn, did) - await obtainLock(storage) - const prevCid = await storage.getRoot() - if (!prevCid) { - throw new InvalidRequestError('Could not find repo') - } - const prev = await storage.readObj(prevCid, def.versionedCommit) - const records = await dbTxn.db - .selectFrom('record') - .select(['collection', 'rkey', 'cid']) - .where('did', '=', did) - .execute() - const memoryStore = new MemoryBlockstore() - let data = await MST.create(memoryStore) - for (const record of records) { - const dataKey = record.collection + '/' + record.rkey - const cid = CID.parse(record.cid) - data = await data.add(dataKey, cid) - } - const dataCid = await data.getPointer() - if (!force && !dataCid.equals(prev.data)) { - throw new InvalidRequestError('Data cid did not match') - } - const recordCids = records.map((r) => r.cid) - const diff = await DataDiff.of(data, null) - const cidsToKeep = [...recordCids, ...diff.newMstBlocks.cids()] - const rev = TID.nextStr(prev.rev) - if (force) { - const got = await storage.getBlocks(diff.newMstBlocks.cids()) - const toAdd = diff.newMstBlocks.getMany(got.missing) - log.info( - { missing: got.missing.length }, - 'force added missing blocks', - ) - // puts any new blocks & no-ops for already existing - await storage.putMany(toAdd.blocks, rev) - } - for (const chunk of chunkArray(cidsToKeep, 500)) { - const cidStrs = chunk.map((c) => c.toString()) - await dbTxn.db - .updateTable('ipld_block') - .set({ repoRev: rev }) - .where('creator', '=', did) - .where('cid', 'in', cidStrs) - .execute() - } - await dbTxn.db - .deleteFrom('ipld_block') - .where('creator', '=', did) - .where((qb) => - qb.where('repoRev', 'is', null).orWhere('repoRev', '!=', rev), - ) - .execute() - await dbTxn.db - .updateTable('repo_blob') - .set({ repoRev: rev }) - .where('did', '=', did) - .execute() - await dbTxn.db - .updateTable('record') - .set({ repoRev: rev }) - .where('did', '=', did) - .execute() - const commit = await signCommit( - { - did, - version: 3, - rev: TID.nextStr(), - prev: prevCid, - data: dataCid, - }, - ctx.repoSigningKey, - ) - const newBlocks = new BlockMap() - const commitCid = await newBlocks.add(commit) - await storage.putMany(newBlocks, rev) - await dbTxn.db - .updateTable('repo_root') - .set({ - root: commitCid.toString(), - rev, - indexedAt: storage.getTimestamp(), - }) - .where('did', '=', did) - .execute() - - const commitData = { - cid: commitCid, - rev, - prev: prevCid, - since: null, - newBlocks, - removedCids: new CidSet(), - } - const seqEvt = await formatSeqCommit(did, commitData, []) - await sequenceEvt(dbTxn, seqEvt) - }) - }, - }) -} - -const obtainLock = async (storage: SqlRepoStorage, tries = 20) => { - const obtained = await storage.lockRepo() - if (obtained) { - return - } - if (tries < 1) { - throw new InvalidRequestError('could not obtain lock') - } - await wait(50) - return obtainLock(storage, tries - 1) -} diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index df15a497c63..028b3cbf397 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -67,7 +67,6 @@ import * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos' import * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate' import * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl' import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos' -import * as ComAtprotoTempUpgradeRepoVersion from './types/com/atproto/temp/upgradeRepoVersion' import * as AppBskyActorGetPreferences from './types/app/bsky/actor/getPreferences' import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile' import * as AppBskyActorGetProfiles from './types/app/bsky/actor/getProfiles' @@ -163,7 +162,6 @@ export class AtprotoNS { repo: RepoNS server: ServerNS sync: SyncNS - temp: TempNS constructor(server: Server) { this._server = server @@ -174,7 +172,6 @@ export class AtprotoNS { this.repo = new RepoNS(server) this.server = new ServerNS(server) this.sync = new SyncNS(server) - this.temp = new TempNS(server) } } @@ -872,25 +869,6 @@ export class SyncNS { } } -export class TempNS { - _server: Server - - constructor(server: Server) { - this._server = server - } - - upgradeRepoVersion( - cfg: ConfigOf< - AV, - ComAtprotoTempUpgradeRepoVersion.Handler>, - ComAtprotoTempUpgradeRepoVersion.HandlerReqCtx> - >, - ) { - const nsid = 'com.atproto.temp.upgradeRepoVersion' // @ts-ignore - return this._server.xrpc.method(nsid, cfg) - } -} - export class AppNS { _server: Server bsky: BskyNS diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 6f1cef89925..30da3464cb2 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -3517,32 +3517,6 @@ export const schemaDict = { }, }, }, - ComAtprotoTempUpgradeRepoVersion: { - lexicon: 1, - id: 'com.atproto.temp.upgradeRepoVersion', - defs: { - main: { - type: 'procedure', - description: 'Upgrade a repo to v3', - input: { - encoding: 'application/json', - schema: { - type: 'object', - required: ['did'], - properties: { - did: { - type: 'string', - format: 'did', - }, - force: { - type: 'boolean', - }, - }, - }, - }, - }, - }, - }, AppBskyActorDefs: { lexicon: 1, id: 'app.bsky.actor.defs', @@ -6828,7 +6802,6 @@ export const ids = { ComAtprotoSyncNotifyOfUpdate: 'com.atproto.sync.notifyOfUpdate', ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl', ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos', - ComAtprotoTempUpgradeRepoVersion: 'com.atproto.temp.upgradeRepoVersion', AppBskyActorDefs: 'app.bsky.actor.defs', AppBskyActorGetPreferences: 'app.bsky.actor.getPreferences', AppBskyActorGetProfile: 'app.bsky.actor.getProfile', diff --git a/packages/pds/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts b/packages/pds/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts deleted file mode 100644 index c5b77876fec..00000000000 --- a/packages/pds/src/lexicon/types/com/atproto/temp/upgradeRepoVersion.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * GENERATED CODE - DO NOT MODIFY - */ -import express from 'express' -import { ValidationResult, BlobRef } from '@atproto/lexicon' -import { lexicons } from '../../../../lexicons' -import { isObj, hasProp } from '../../../../util' -import { CID } from 'multiformats/cid' -import { HandlerAuth } from '@atproto/xrpc-server' - -export interface QueryParams {} - -export interface InputSchema { - did: string - force?: boolean - [k: string]: unknown -} - -export interface HandlerInput { - encoding: 'application/json' - body: InputSchema -} - -export interface HandlerError { - status: number - message?: string -} - -export type HandlerOutput = HandlerError | void -export type HandlerReqCtx = { - auth: HA - params: QueryParams - input: HandlerInput - req: express.Request - res: express.Response -} -export type Handler = ( - ctx: HandlerReqCtx, -) => Promise | HandlerOutput diff --git a/packages/pds/tests/migrations/repo-version-upgrade.test.ts b/packages/pds/tests/migrations/repo-version-upgrade.test.ts deleted file mode 100644 index d73bc8cc7ae..00000000000 --- a/packages/pds/tests/migrations/repo-version-upgrade.test.ts +++ /dev/null @@ -1,173 +0,0 @@ -import AtpAgent from '@atproto/api' -import { SeedClient } from '../seeds/client' -import basicSeed from '../seeds/basic' -import { TestNetworkNoAppView } from '@atproto/dev-env' -import { randomBytes } from 'crypto' -import { TID, cidForCbor } from '@atproto/common' -import { IpldBlock } from '../../src/db/tables/ipld-block' -import { readCarWithRoot, verifyRepo } from '@atproto/repo' -import { Database } from '../../src' - -describe('repo version upgrade', () => { - let network: TestNetworkNoAppView - let db: Database - let agent: AtpAgent - let sc: SeedClient - - let alice: string - - beforeAll(async () => { - network = await TestNetworkNoAppView.create({ - dbPostgresSchema: 'repo_version_upgrade', - }) - db = network.pds.ctx.db - agent = network.pds.getClient() - sc = new SeedClient(agent) - await basicSeed(sc) - alice = sc.dids.alice - }) - - afterAll(async () => { - await network.close() - }) - - const getNonAliceData = async () => { - const ipldBlocksQb = db.db - .selectFrom('ipld_block') - .where('creator', '!=', alice) - .selectAll() - .orderBy('creator') - .orderBy('cid') - const recordsQb = db.db - .selectFrom('record') - .where('did', '!=', alice) - .selectAll() - .orderBy('did') - .orderBy('uri') - const repoBlobsQb = db.db - .selectFrom('repo_blob') - .where('did', '!=', alice) - .selectAll() - .orderBy('did') - .orderBy('cid') - const repoRootsQb = db.db - .selectFrom('repo_root') - .where('did', '!=', alice) - .selectAll() - .orderBy('did') - const [ipldBlocks, records, repoBlobs, repoRoots] = await Promise.all([ - ipldBlocksQb.execute(), - recordsQb.execute(), - repoBlobsQb.execute(), - repoRootsQb.execute(), - ]) - return { - ipldBlocks, - records, - repoBlobs, - repoRoots, - } - } - - const addCruft = async (did: string) => { - const cruft: IpldBlock[] = [] - for (let i = 0; i < 1000; i++) { - const bytes = randomBytes(128) - const cid = await cidForCbor(bytes) - cruft.push({ - cid: cid.toString(), - creator: did, - repoRev: Math.random() > 0.5 ? TID.nextStr() : null, - size: 128, - content: bytes, - }) - } - await db.db.insertInto('ipld_block').values(cruft).execute() - return cruft - } - - const fetchAndVerifyRepo = async (did: string) => { - const res = await agent.api.com.atproto.sync.getRepo({ - did, - }) - const car = await readCarWithRoot(res.data) - return verifyRepo( - car.blocks, - car.root, - alice, - network.pds.ctx.repoSigningKey.did(), - ) - } - - it('upgrades a repo', async () => { - const nonAliceDataBefore = await getNonAliceData() - const aliceRepoBefore = await fetchAndVerifyRepo(alice) - - const cruft = await addCruft(alice) - - await agent.api.com.atproto.temp.upgradeRepoVersion( - { did: alice }, - { - headers: network.pds.adminAuthHeaders('admin'), - encoding: 'application/json', - }, - ) - - const nonAliceDataAfter = await getNonAliceData() - - // does not affect other users - expect(nonAliceDataAfter).toEqual(nonAliceDataBefore) - - // cleans up cruft - const res = await db.db - .selectFrom('ipld_block') - .selectAll() - .where('creator', '=', alice) - .execute() - const cidSet = new Set(res.map((row) => row.cid)) - for (const row of cruft) { - expect(cidSet.has(row.cid)).toBe(false) - } - - const aliceRepoAfter = await fetchAndVerifyRepo(alice) - expect(aliceRepoAfter.creates).toEqual(aliceRepoBefore.creates) - - // it updated the repo rev on all blocks/records/blobs - const root = await db.db - .selectFrom('repo_root') - .where('did', '=', alice) - .selectAll() - .executeTakeFirst() - if (!root || !root.rev) { - throw new Error('did not set rev') - } - expect(root.root).toEqual(aliceRepoAfter.commit.cid.toString()) - const nonUpgradedRecords = await db.db - .selectFrom('record') - .where('did', '=', alice) - .where((qb) => - qb.where('repoRev', '!=', root.rev).orWhere('repoRev', 'is', null), - ) - .selectAll() - .execute() - expect(nonUpgradedRecords.length).toBe(0) - const nonUpgradedBlocks = await db.db - .selectFrom('ipld_block') - .where('creator', '=', alice) - .where((qb) => - qb.where('repoRev', '!=', root.rev).orWhere('repoRev', 'is', null), - ) - .selectAll() - .execute() - expect(nonUpgradedBlocks.length).toBe(0) - const nonUpgradedBlobs = await db.db - .selectFrom('repo_blob') - .where('did', '=', alice) - .where((qb) => - qb.where('repoRev', '!=', root.rev).orWhere('repoRev', 'is', null), - ) - .selectAll() - .execute() - expect(nonUpgradedBlobs.length).toBe(0) - }) -}) From 0533fab68ea32df4e00948ddfc2422c6f900223a Mon Sep 17 00:00:00 2001 From: Daniel Holmgren Date: Tue, 12 Sep 2023 12:38:20 -0500 Subject: [PATCH 235/237] Get rate limit ip correctly (#1577) * get rate limit ip correctly * comment --- packages/pds/src/index.ts | 1 + packages/xrpc-server/src/index.ts | 2 +- packages/xrpc-server/src/rate-limiter.ts | 5 +++-- packages/xrpc-server/src/util.ts | 4 ---- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/pds/src/index.ts b/packages/pds/src/index.ts index 32abb30056d..dc7e36b8d62 100644 --- a/packages/pds/src/index.ts +++ b/packages/pds/src/index.ts @@ -137,6 +137,7 @@ export class PDS { ) const app = express() + app.set('trust proxy', true) app.use(cors()) app.use(loggerMiddleware) app.use(compression()) diff --git a/packages/xrpc-server/src/index.ts b/packages/xrpc-server/src/index.ts index 6bc4147cb44..1458d2ba070 100644 --- a/packages/xrpc-server/src/index.ts +++ b/packages/xrpc-server/src/index.ts @@ -5,4 +5,4 @@ export * from './stream' export * from './rate-limiter' export type { ServerTiming } from './util' -export { getReqIp, serverTimingHeader, ServerTimer } from './util' +export { serverTimingHeader, ServerTimer } from './util' diff --git a/packages/xrpc-server/src/rate-limiter.ts b/packages/xrpc-server/src/rate-limiter.ts index e650ec599ba..82719101674 100644 --- a/packages/xrpc-server/src/rate-limiter.ts +++ b/packages/xrpc-server/src/rate-limiter.ts @@ -14,7 +14,6 @@ import { RateLimiterStatus, XRPCReqContext, } from './types' -import { getReqIp } from './util' export type RateLimiterOpts = { keyPrefix: string @@ -155,5 +154,7 @@ export const getTightestLimit = ( return lowest } -const defaultKey: CalcKeyFn = (ctx: XRPCReqContext) => getReqIp(ctx.req) +// when using a proxy, ensure headers are getting forwarded correctly: `app.set('trust proxy', true)` +// https://expressjs.com/en/guide/behind-proxies.html +const defaultKey: CalcKeyFn = (ctx: XRPCReqContext) => ctx.req.ip const defaultPoints: CalcPointsFn = () => 1 diff --git a/packages/xrpc-server/src/util.ts b/packages/xrpc-server/src/util.ts index 813587382ba..730db950fbd 100644 --- a/packages/xrpc-server/src/util.ts +++ b/packages/xrpc-server/src/util.ts @@ -268,10 +268,6 @@ function decodeBodyStream( return stream } -export const getReqIp = (req: express.Request): string => { - return req.ips.at(-1) ?? req.ip -} - export function serverTimingHeader(timings: ServerTiming[]) { return timings .map((timing) => { From abc6cf9ab4f3bcea02b3fe3637bb2bd520ed8edf Mon Sep 17 00:00:00 2001 From: bnewbold Date: Wed, 13 Sep 2023 09:23:33 -0700 Subject: [PATCH 236/237] interop test files (#1529) * initial interop-test-files * crypto: switch signature-fixtures.json to a symlink * syntax: test against interop files * prettier * Update interop-test-files/README.md Co-authored-by: Eric Bailey * disable prettier on test vectors --------- Co-authored-by: Eric Bailey Co-authored-by: dholms --- .prettierignore | 1 + interop-test-files/README.md | 9 ++ .../crypto/signature-fixtures.json | 42 +++++++++ .../crypto/w3c_didkey_K256.json | 22 +++++ .../crypto/w3c_didkey_P256.json | 6 ++ .../syntax/atidentifier_syntax_invalid.txt | 28 ++++++ .../syntax/atidentifier_syntax_valid.txt | 15 ++++ .../syntax/aturi_syntax_invalid.txt | 89 ++++++++++++++++++ .../syntax/aturi_syntax_valid.txt | 26 ++++++ .../syntax/did_syntax_invalid.txt | 19 ++++ .../syntax/did_syntax_valid.txt | 26 ++++++ .../syntax/handle_syntax_invalid.txt | 61 +++++++++++++ .../syntax/handle_syntax_valid.txt | 90 +++++++++++++++++++ .../syntax/nsid_syntax_invalid.txt | 32 +++++++ .../syntax/nsid_syntax_valid.txt | 29 ++++++ .../syntax/recordkey_syntax_invalid.txt | 14 +++ .../syntax/recordkey_syntax_valid.txt | 8 ++ packages/crypto/tests/signature-fixtures.json | 35 +------- packages/syntax/tests/aturi.test.ts | 19 ++++ packages/syntax/tests/did.test.ts | 32 +++++++ packages/syntax/tests/handle.test.ts | 32 +++++++ .../interop-files/aturi_syntax_invalid.txt | 1 + .../interop-files/aturi_syntax_valid.txt | 1 + .../interop-files/did_syntax_invalid.txt | 1 + .../tests/interop-files/did_syntax_valid.txt | 1 + .../interop-files/handle_syntax_invalid.txt | 1 + .../interop-files/handle_syntax_valid.txt | 1 + .../interop-files/nsid_syntax_invalid.txt | 1 + .../tests/interop-files/nsid_syntax_valid.txt | 1 + packages/syntax/tests/nsid.test.ts | 32 +++++++ 30 files changed, 641 insertions(+), 34 deletions(-) create mode 100644 interop-test-files/README.md create mode 100644 interop-test-files/crypto/signature-fixtures.json create mode 100644 interop-test-files/crypto/w3c_didkey_K256.json create mode 100644 interop-test-files/crypto/w3c_didkey_P256.json create mode 100644 interop-test-files/syntax/atidentifier_syntax_invalid.txt create mode 100644 interop-test-files/syntax/atidentifier_syntax_valid.txt create mode 100644 interop-test-files/syntax/aturi_syntax_invalid.txt create mode 100644 interop-test-files/syntax/aturi_syntax_valid.txt create mode 100644 interop-test-files/syntax/did_syntax_invalid.txt create mode 100644 interop-test-files/syntax/did_syntax_valid.txt create mode 100644 interop-test-files/syntax/handle_syntax_invalid.txt create mode 100644 interop-test-files/syntax/handle_syntax_valid.txt create mode 100644 interop-test-files/syntax/nsid_syntax_invalid.txt create mode 100644 interop-test-files/syntax/nsid_syntax_valid.txt create mode 100644 interop-test-files/syntax/recordkey_syntax_invalid.txt create mode 100644 interop-test-files/syntax/recordkey_syntax_valid.txt mode change 100644 => 120000 packages/crypto/tests/signature-fixtures.json create mode 120000 packages/syntax/tests/interop-files/aturi_syntax_invalid.txt create mode 120000 packages/syntax/tests/interop-files/aturi_syntax_valid.txt create mode 120000 packages/syntax/tests/interop-files/did_syntax_invalid.txt create mode 120000 packages/syntax/tests/interop-files/did_syntax_valid.txt create mode 120000 packages/syntax/tests/interop-files/handle_syntax_invalid.txt create mode 120000 packages/syntax/tests/interop-files/handle_syntax_valid.txt create mode 120000 packages/syntax/tests/interop-files/nsid_syntax_invalid.txt create mode 120000 packages/syntax/tests/interop-files/nsid_syntax_valid.txt diff --git a/.prettierignore b/.prettierignore index f45dcc122d0..6340cc359c2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ node_modules +interop-test-files dist build .nyc_output diff --git a/interop-test-files/README.md b/interop-test-files/README.md new file mode 100644 index 00000000000..3a72400317c --- /dev/null +++ b/interop-test-files/README.md @@ -0,0 +1,9 @@ + +atproto Interop Test Files +========================== + +This directory contains reusable files for testing interoperability and specification compliance for atproto (AT Protocol). + +The protocol itself is documented at . If there are conflicts or ambiguity between these test files and the specs, the specs are the authority, and these test files should usually be corrected. + +These files are intended to be simple (JSON, text files, etc) and mostly self-documenting. diff --git a/interop-test-files/crypto/signature-fixtures.json b/interop-test-files/crypto/signature-fixtures.json new file mode 100644 index 00000000000..917c6d02455 --- /dev/null +++ b/interop-test-files/crypto/signature-fixtures.json @@ -0,0 +1,42 @@ +[ + { + "comment": "valid P-256 key and signature, with low-S signature", + "messageBase64": "oWVoZWxsb2V3b3JsZA", + "algorithm": "ES256", + "didDocSuite": "EcdsaSecp256r1VerificationKey2019", + "publicKeyDid": "did:key:zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo", + "publicKeyMultibase": "zxdM8dSstjrpZaRUwBmDvjGXweKuEMVN95A9oJBFjkWMh", + "signatureBase64": "2vZNsG3UKvvO/CDlrdvyZRISOFylinBh0Jupc6KcWoJWExHptCfduPleDbG3rko3YZnn9Lw0IjpixVmexJDegg", + "validSignature": true + }, + { + "comment": "valid K-256 key and signature, with low-S signature", + "messageBase64": "oWVoZWxsb2V3b3JsZA", + "algorithm": "ES256K", + "didDocSuite": "EcdsaSecp256k1VerificationKey2019", + "publicKeyDid": "did:key:zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc", + "publicKeyMultibase": "z25z9DTpsiYYJKGsWmSPJK2NFN8PcJtZig12K59UgW7q5t", + "signatureBase64": "5WpdIuEUUfVUYaozsi8G0B3cWO09cgZbIIwg1t2YKdUn/FEznOndsz/qgiYb89zwxYCbB71f7yQK5Lr7NasfoA", + "validSignature": true + }, + { + "comment": "P-256 key and signature, with non-low-S signature which is invalid in atproto", + "messageBase64": "oWVoZWxsb2V3b3JsZA", + "algorithm": "ES256", + "didDocSuite": "EcdsaSecp256r1VerificationKey2019", + "publicKeyDid": "did:key:zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo", + "publicKeyMultibase": "zxdM8dSstjrpZaRUwBmDvjGXweKuEMVN95A9oJBFjkWMh", + "signatureBase64": "2vZNsG3UKvvO/CDlrdvyZRISOFylinBh0Jupc6KcWoKp7O4VS9giSAah8k5IUbXIW00SuOrjfEqQ9HEkN9JGzw", + "validSignature": false + }, + { + "comment": "K-256 key and signature, with non-low-S signature which is invalid in atproto", + "messageBase64": "oWVoZWxsb2V3b3JsZA", + "algorithm": "ES256K", + "didDocSuite": "EcdsaSecp256k1VerificationKey2019", + "publicKeyDid": "did:key:zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc", + "publicKeyMultibase": "z25z9DTpsiYYJKGsWmSPJK2NFN8PcJtZig12K59UgW7q5t", + "signatureBase64": "5WpdIuEUUfVUYaozsi8G0B3cWO09cgZbIIwg1t2YKdXYA67MYxYiTMAVfdnkDCMN9S5B3vHosRe07aORmoshoQ", + "validSignature": false + } +] diff --git a/interop-test-files/crypto/w3c_didkey_K256.json b/interop-test-files/crypto/w3c_didkey_K256.json new file mode 100644 index 00000000000..9ba9844b3ed --- /dev/null +++ b/interop-test-files/crypto/w3c_didkey_K256.json @@ -0,0 +1,22 @@ +[ + { + "privateKeyBytesHex": "9085d2bef69286a6cbb51623c8fa258629945cd55ca705cc4e66700396894e0c", + "publicDidKey": "did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme" + }, + { + "privateKeyBytesHex": "f0f4df55a2b3ff13051ea814a8f24ad00f2e469af73c363ac7e9fb999a9072ed", + "publicDidKey": "did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2" + }, + { + "privateKeyBytesHex": "6b0b91287ae3348f8c2f2552d766f30e3604867e34adc37ccbb74a8e6b893e02", + "publicDidKey": "did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N" + }, + { + "privateKeyBytesHex": "c0a6a7c560d37d7ba81ecee9543721ff48fea3e0fb827d42c1868226540fac15", + "publicDidKey": "did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy" + }, + { + "privateKeyBytesHex": "175a232d440be1e0788f25488a73d9416c04b6f924bea6354bf05dd2f1a75133", + "publicDidKey": "did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj" + } +] diff --git a/interop-test-files/crypto/w3c_didkey_P256.json b/interop-test-files/crypto/w3c_didkey_P256.json new file mode 100644 index 00000000000..65d50933a5f --- /dev/null +++ b/interop-test-files/crypto/w3c_didkey_P256.json @@ -0,0 +1,6 @@ +[ + { + "privateKeyBytesBase58": "9p4VRzdmhsnq869vQjVCTrRry7u4TtfRxhvBFJTGU2Cp", + "publicDidKey": "did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb" + } +] diff --git a/interop-test-files/syntax/atidentifier_syntax_invalid.txt b/interop-test-files/syntax/atidentifier_syntax_invalid.txt new file mode 100644 index 00000000000..f0f84309b3e --- /dev/null +++ b/interop-test-files/syntax/atidentifier_syntax_invalid.txt @@ -0,0 +1,28 @@ + +# invalid handles +did:thing.test +did:thing +john-.test +john.0 +john.- +xn--bcher-.tld +john..test +jo_hn.test + +# invalid DIDs +did +didmethodval +method:did:val +did:method: +didmethod:val +did:methodval) +:did:method:val +did:method:val: +did:method:val% +DID:method:val + +# other invalid stuff +email@example.com +@handle@example.com +@handle +blah diff --git a/interop-test-files/syntax/atidentifier_syntax_valid.txt b/interop-test-files/syntax/atidentifier_syntax_valid.txt new file mode 100644 index 00000000000..cc4a42b0fa7 --- /dev/null +++ b/interop-test-files/syntax/atidentifier_syntax_valid.txt @@ -0,0 +1,15 @@ + +# allows valid handles +XX.LCS.MIT.EDU +john.test +jan.test +a234567890123456789.test +john2.test +john-john.test + +# allows valid DIDs +did:method:val +did:method:VAL +did:method:val123 +did:method:123 +did:method:val-two diff --git a/interop-test-files/syntax/aturi_syntax_invalid.txt b/interop-test-files/syntax/aturi_syntax_invalid.txt new file mode 100644 index 00000000000..2ac2eadadb3 --- /dev/null +++ b/interop-test-files/syntax/aturi_syntax_invalid.txt @@ -0,0 +1,89 @@ + +# enforces spec basics +a://did:plc:asdf123 +at//did:plc:asdf123 +at:/a/did:plc:asdf123 +at:/did:plc:asdf123 +AT://did:plc:asdf123 +http://did:plc:asdf123 +://did:plc:asdf123 +at:did:plc:asdf123 +at:/did:plc:asdf123 +at:///did:plc:asdf123 +at://:/did:plc:asdf123 +at:/ /did:plc:asdf123 +at://did:plc:asdf123 +at://did:plc:asdf123/ + at://did:plc:asdf123 +at://did:plc:asdf123/com.atproto.feed.post +at://did:plc:asdf123/com.atproto.feed.post# +at://did:plc:asdf123/com.atproto.feed.post#/ +at://did:plc:asdf123/com.atproto.feed.post#/frag +at://did:plc:asdf123/com.atproto.feed.post#fr ag +//did:plc:asdf123 +at://name +at://name.0 +at://diD:plc:asdf123 +at://did:plc:asdf123/com.atproto.feed.p@st +at://did:plc:asdf123/com.atproto.feed.p$st +at://did:plc:asdf123/com.atproto.feed.p%st +at://did:plc:asdf123/com.atproto.feed.p&st +at://did:plc:asdf123/com.atproto.feed.p()t +at://did:plc:asdf123/com.atproto.feed_post +at://did:plc:asdf123/-com.atproto.feed.post +at://did:plc:asdf@123/com.atproto.feed.post +at://DID:plc:asdf123 +at://user.bsky.123 +at://bsky +at://did:plc: +at://did:plc: +at://frag + +# too long: 'at://did:plc:asdf123/com.atproto.feed.post/' + 'o'.repeat(8200) +at://did:plc:asdf123/com.atproto.feed.post/oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + +# has specified behavior on edge cases +at://user.bsky.social// +at://user.bsky.social//com.atproto.feed.post +at://user.bsky.social/com.atproto.feed.post// +at://did:plc:asdf123/com.atproto.feed.post/asdf123/more/more', +at://did:plc:asdf123/short/stuff +at://did:plc:asdf123/12345 + +# enforces no trailing slashes +at://did:plc:asdf123/ +at://user.bsky.social/ +at://did:plc:asdf123/com.atproto.feed.post/ +at://did:plc:asdf123/com.atproto.feed.post/record/ +at://did:plc:asdf123/com.atproto.feed.post/record/#/frag + +# enforces strict paths +at://did:plc:asdf123/com.atproto.feed.post/asdf123/asdf + +# is very permissive about fragments +at://did:plc:asdf123# +at://did:plc:asdf123## +#at://did:plc:asdf123 +at://did:plc:asdf123#/asdf#/asdf + +# new less permissive about record keys for Lexicon use (with recordkey more specified) +at://did:plc:asdf123/com.atproto.feed.post/%23 +at://did:plc:asdf123/com.atproto.feed.post/$@!*)(:,;~.sdf123 +at://did:plc:asdf123/com.atproto.feed.post/~'sdf123") +at://did:plc:asdf123/com.atproto.feed.post/$ +at://did:plc:asdf123/com.atproto.feed.post/@ +at://did:plc:asdf123/com.atproto.feed.post/! +at://did:plc:asdf123/com.atproto.feed.post/* +at://did:plc:asdf123/com.atproto.feed.post/( +at://did:plc:asdf123/com.atproto.feed.post/, +at://did:plc:asdf123/com.atproto.feed.post/; +at://did:plc:asdf123/com.atproto.feed.post/abc%30123 +at://did:plc:asdf123/com.atproto.feed.post/%30 +at://did:plc:asdf123/com.atproto.feed.post/%3 +at://did:plc:asdf123/com.atproto.feed.post/% +at://did:plc:asdf123/com.atproto.feed.post/%zz +at://did:plc:asdf123/com.atproto.feed.post/%%% + +# disallow dot / double-dot +at://did:plc:asdf123/com.atproto.feed.post/. +at://did:plc:asdf123/com.atproto.feed.post/.. diff --git a/interop-test-files/syntax/aturi_syntax_valid.txt b/interop-test-files/syntax/aturi_syntax_valid.txt new file mode 100644 index 00000000000..2552a964ce0 --- /dev/null +++ b/interop-test-files/syntax/aturi_syntax_valid.txt @@ -0,0 +1,26 @@ + +# enforces spec basics +at://did:plc:asdf123 +at://user.bsky.social +at://did:plc:asdf123/com.atproto.feed.post +at://did:plc:asdf123/com.atproto.feed.post/record + +# very long: 'at://did:plc:asdf123/com.atproto.feed.post/' + 'o'.repeat(512) +at://did:plc:asdf123/com.atproto.feed.post/oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo + +# enforces no trailing slashes +at://did:plc:asdf123 +at://user.bsky.social +at://did:plc:asdf123/com.atproto.feed.post +at://did:plc:asdf123/com.atproto.feed.post/record + +# enforces strict paths +at://did:plc:asdf123/com.atproto.feed.post/asdf123 + +# is very permissive about record keys +at://did:plc:asdf123/com.atproto.feed.post/asdf123 +at://did:plc:asdf123/com.atproto.feed.post/a + +at://did:plc:asdf123/com.atproto.feed.post/asdf-123 +at://did:abc:123 +at://did:abc:123/io.nsid.someFunc/record-key diff --git a/interop-test-files/syntax/did_syntax_invalid.txt b/interop-test-files/syntax/did_syntax_invalid.txt new file mode 100644 index 00000000000..9e724b3d7b5 --- /dev/null +++ b/interop-test-files/syntax/did_syntax_invalid.txt @@ -0,0 +1,19 @@ +did +didmethodval +method:did:val +did:method: +didmethod:val +did:methodval) +:did:method:val +did.method.val +did:method:val: +did:method:val% +DID:method:val +did:METHOD:val +did:m123:val +did:method:val/two +did:method:val?two +did:method:val#two +did:method:val% +did:method:vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + diff --git a/interop-test-files/syntax/did_syntax_valid.txt b/interop-test-files/syntax/did_syntax_valid.txt new file mode 100644 index 00000000000..5aa6c9e7e3b --- /dev/null +++ b/interop-test-files/syntax/did_syntax_valid.txt @@ -0,0 +1,26 @@ +did:method:val +did:method:VAL +did:method:val123 +did:method:123 +did:method:val-two +did:method:val_two +did:method:val.two +did:method:val:two +did:method:val%BB +did:method:vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +did:m:v +did:method::::val +did:method:- +did:method:-:_:.:%ab +did:method:. +did:method:_ +did:method::. + +# allows some real DID values +did:onion:2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid +did:example:123456789abcdefghi +did:plc:7iza6de2dwap2sbkpav7c6c6 +did:web:example.com +did:web:localhost%3A1234 +did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N +did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a diff --git a/interop-test-files/syntax/handle_syntax_invalid.txt b/interop-test-files/syntax/handle_syntax_invalid.txt new file mode 100644 index 00000000000..49275a390bf --- /dev/null +++ b/interop-test-files/syntax/handle_syntax_invalid.txt @@ -0,0 +1,61 @@ +# throws on invalid handles +did:thing.test +did:thing +john-.test +john.0 +john.- +xn--bcher-.tld +john..test +jo_hn.test +-john.test +.john.test +jo!hn.test +jo%hn.test +jo&hn.test +jo@hn.test +jo*hn.test +jo|hn.test +jo:hn.test +jo/hn.test +john💩.test +bücher.test +john .test +john.test. +john +john. +.john +john.test. +.john.test + john.test +john.test +joh-.test +john.-est +john.tes- + +# max over all handle: 'shoooort' + '.loooooooooooooooooooooooooong'.repeat(9) + '.test' +shoooort.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.test + +# max segment: 'short.' + 'o'.repeat(64) + '.test' +short.oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.test + +# throws on "dotless" TLD handles +org +ai +gg +io + +# correctly validates corner cases (modern vs. old RFCs) +cn.8 +thing.0aa +thing.0aa + +# does not allow IP addresses as handles +127.0.0.1 +192.168.0.142 +fe80::7325:8a97:c100:94b +2600:3c03::f03c:9100:feb0:af1f + +# examples from stackoverflow +-notvalid.at-all +-thing.com +www.masełkowski.pl.com diff --git a/interop-test-files/syntax/handle_syntax_valid.txt b/interop-test-files/syntax/handle_syntax_valid.txt new file mode 100644 index 00000000000..a23a9213839 --- /dev/null +++ b/interop-test-files/syntax/handle_syntax_valid.txt @@ -0,0 +1,90 @@ +# allows valid handles +A.ISI.EDU +XX.LCS.MIT.EDU +SRI-NIC.ARPA +john.test +jan.test +a234567890123456789.test +john2.test +john-john.test +john.bsky.app +jo.hn +a.co +a.org +joh.n +j0.h0 +jaymome-johnber123456.test +jay.mome-johnber123456.test +john.test.bsky.app + +# max over all handle: 'shoooort' + '.loooooooooooooooooooooooooong'.repeat(8) + '.test' +shoooort.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.loooooooooooooooooooooooooong.test + +# max segment: 'short.' + 'o'.repeat(63) + '.test' +short.ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.test + +# NOTE: this probably isn't ever going to be a real domain, but my read of the RFC is that it would be possible +john.t + +# allows .local and .arpa handles (proto-level) +laptop.local +laptop.arpa + +# allows punycode handles +# 💩.test +xn--ls8h.test +# bücher.tld +xn--bcher-kva.tld +xn--3jk.com +xn--w3d.com +xn--vqb.com +xn--ppd.com +xn--cs9a.com +xn--8r9a.com +xn--cfd.com +xn--5jk.com +xn--2lb.com + +# allows onion (Tor) handles +expyuzz4wqqyqhjn.onion +friend.expyuzz4wqqyqhjn.onion +g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion +friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion +friend.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion +2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion +friend.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion + +# correctly validates corner cases (modern vs. old RFCs) +12345.test +8.cn +4chan.org +4chan.o-g +blah.4chan.org +thing.a01 +120.0.0.1.com +0john.test +9sta--ck.com +99stack.com +0ohn.test +john.t--t +thing.0aa.thing + +# examples from stackoverflow +stack.com +sta-ck.com +sta---ck.com +sta--ck9.com +stack99.com +sta99ck.com +google.com.uk +google.co.in +google.com +maselkowski.pl +m.maselkowski.pl +xn--masekowski-d0b.pl +xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s +xn--stackoverflow.com +stackoverflow.xn--com +stackoverflow.co.uk +xn--masekowski-d0b.pl +xn--fiqa61au8b7zsevnm8ak20mc4a87e.xn--fiqs8s diff --git a/interop-test-files/syntax/nsid_syntax_invalid.txt b/interop-test-files/syntax/nsid_syntax_invalid.txt new file mode 100644 index 00000000000..bc0bd2fdf86 --- /dev/null +++ b/interop-test-files/syntax/nsid_syntax_invalid.txt @@ -0,0 +1,32 @@ +# length checks +com.oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.foo +com.example.oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +com.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.foo + +# invliad examples +com.example.foo.* +com.example.foo.blah* +com.example.foo.*blah +com.example.f00 +com.exa💩ple.thing +a-0.b-1.c-3 +a-0.b-1.c-o +a0.b1.c3 +1.0.0.127.record +0two.example.foo +example.com +com.example +a. +.one.two.three +one.two.three +one.two..three +one .two.three + one.two.three +com.exa💩ple.thing +com.atproto.feed.p@st +com.atproto.feed.p_st +com.atproto.feed.p*st +com.atproto.feed.po#t +com.atproto.feed.p!ot +com.example-.foo + diff --git a/interop-test-files/syntax/nsid_syntax_valid.txt b/interop-test-files/syntax/nsid_syntax_valid.txt new file mode 100644 index 00000000000..54ef351f077 --- /dev/null +++ b/interop-test-files/syntax/nsid_syntax_valid.txt @@ -0,0 +1,29 @@ +# length checks +com.ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.foo +com.example.ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo +com.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.middle.foo + +# valid examples +com.example.fooBar +net.users.bob.ping +a.b.c +m.xn--masekowski-d0b.pl +one.two.three +one.two.three.four-and.FiVe +one.2.three +a-0.b-1.c +a0.b1.cc +cn.8.lex.stuff +test.12345.record +a01.thing.record +a.0.c +xn--fiqs8s.xn--fiqa61au8b7zsevnm8ak20mc4a87e.record.two + +# allows onion (Tor) NSIDs +onion.expyuzz4wqqyqhjn.spec.getThing +onion.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing + +# allows starting-with-numeric segments (same as domains) +org.4chan.lex.getThing +cn.8.lex.stuff +onion.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing diff --git a/interop-test-files/syntax/recordkey_syntax_invalid.txt b/interop-test-files/syntax/recordkey_syntax_invalid.txt new file mode 100644 index 00000000000..1da3d1e7dbc --- /dev/null +++ b/interop-test-files/syntax/recordkey_syntax_invalid.txt @@ -0,0 +1,14 @@ +# specs +literal:self +alpha/beta +. +.. +#extra +@handle +any space +any+space +number[3] +number(3) +"quote" +pre:fix +dHJ1ZQ== diff --git a/interop-test-files/syntax/recordkey_syntax_valid.txt b/interop-test-files/syntax/recordkey_syntax_valid.txt new file mode 100644 index 00000000000..8d77d04d2b7 --- /dev/null +++ b/interop-test-files/syntax/recordkey_syntax_valid.txt @@ -0,0 +1,8 @@ +# specs +self +example.com +~1.2-3_ +dHJ1ZQ + +# very long: 'o'.repeat(512) +oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo diff --git a/packages/crypto/tests/signature-fixtures.json b/packages/crypto/tests/signature-fixtures.json deleted file mode 100644 index 44102239bb3..00000000000 --- a/packages/crypto/tests/signature-fixtures.json +++ /dev/null @@ -1,34 +0,0 @@ -[ - { - "messageBase64": "oWVoZWxsb2V3b3JsZA", - "algorithm": "ES256", - "publicKeyDid": "did:key:zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo", - "publicKeyMultibase": "zxdM8dSstjrpZaRUwBmDvjGXweKuEMVN95A9oJBFjkWMh", - "signatureBase64": "2vZNsG3UKvvO/CDlrdvyZRISOFylinBh0Jupc6KcWoJWExHptCfduPleDbG3rko3YZnn9Lw0IjpixVmexJDegg", - "validSignature": true - }, - { - "messageBase64": "oWVoZWxsb2V3b3JsZA", - "algorithm": "ES256K", - "publicKeyDid": "did:key:zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc", - "publicKeyMultibase": "z25z9DTpsiYYJKGsWmSPJK2NFN8PcJtZig12K59UgW7q5t", - "signatureBase64": "5WpdIuEUUfVUYaozsi8G0B3cWO09cgZbIIwg1t2YKdUn/FEznOndsz/qgiYb89zwxYCbB71f7yQK5Lr7NasfoA", - "validSignature": true - }, - { - "messageBase64": "oWVoZWxsb2V3b3JsZA", - "algorithm": "ES256", - "publicKeyDid": "did:key:zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo", - "publicKeyMultibase": "zxdM8dSstjrpZaRUwBmDvjGXweKuEMVN95A9oJBFjkWMh", - "signatureBase64": "2vZNsG3UKvvO/CDlrdvyZRISOFylinBh0Jupc6KcWoKp7O4VS9giSAah8k5IUbXIW00SuOrjfEqQ9HEkN9JGzw", - "validSignature": false - }, - { - "messageBase64": "oWVoZWxsb2V3b3JsZA", - "algorithm": "ES256K", - "publicKeyDid": "did:key:zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc", - "publicKeyMultibase": "z25z9DTpsiYYJKGsWmSPJK2NFN8PcJtZig12K59UgW7q5t", - "signatureBase64": "5WpdIuEUUfVUYaozsi8G0B3cWO09cgZbIIwg1t2YKdXYA67MYxYiTMAVfdnkDCMN9S5B3vHosRe07aORmoshoQ", - "validSignature": false - } -] diff --git a/packages/crypto/tests/signature-fixtures.json b/packages/crypto/tests/signature-fixtures.json new file mode 120000 index 00000000000..b8aa5efefdd --- /dev/null +++ b/packages/crypto/tests/signature-fixtures.json @@ -0,0 +1 @@ +../../../interop-test-files/crypto/signature-fixtures.json \ No newline at end of file diff --git a/packages/syntax/tests/aturi.test.ts b/packages/syntax/tests/aturi.test.ts index 7506feb8364..dbc31652403 100644 --- a/packages/syntax/tests/aturi.test.ts +++ b/packages/syntax/tests/aturi.test.ts @@ -1,4 +1,6 @@ import { AtUri, ensureValidAtUri, ensureValidAtUriRegex } from '../src/index' +import * as readline from 'readline' +import * as fs from 'fs' describe('At Uris', () => { it('parses valid at uris', () => { @@ -503,4 +505,21 @@ describe('AtUri validation', () => { expectValid('at://did:plc:asdf123#/;') expectValid('at://did:plc:asdf123#/,') }) + + it('conforms to interop valid ATURIs', () => { + const lineReader = readline.createInterface({ + input: fs.createReadStream( + `${__dirname}/interop-files/aturi_syntax_valid.txt`, + ), + terminal: false, + }) + lineReader.on('line', (line) => { + if (line.startsWith('#') || line.length == 0) { + return + } + expectValid(line) + }) + }) + + // NOTE: this package is currently more permissive than spec about AT URIs, so invalid cases are not errors }) diff --git a/packages/syntax/tests/did.test.ts b/packages/syntax/tests/did.test.ts index 5da98d703cb..d3408945828 100644 --- a/packages/syntax/tests/did.test.ts +++ b/packages/syntax/tests/did.test.ts @@ -1,4 +1,6 @@ import { ensureValidDid, ensureValidDidRegex, InvalidDidError } from '../src' +import * as readline from 'readline' +import * as fs from 'fs' describe('DID permissive validation', () => { const expectValid = (h: string) => { @@ -64,4 +66,34 @@ describe('DID permissive validation', () => { expectValid('did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N') expectValid('did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a') }) + + it('conforms to interop valid DIDs', () => { + const lineReader = readline.createInterface({ + input: fs.createReadStream( + `${__dirname}/interop-files/did_syntax_valid.txt`, + ), + terminal: false, + }) + lineReader.on('line', (line) => { + if (line.startsWith('#') || line.length == 0) { + return + } + expectValid(line) + }) + }) + + it('conforms to interop invalid DIDs', () => { + const lineReader = readline.createInterface({ + input: fs.createReadStream( + `${__dirname}/interop-files/did_syntax_invalid.txt`, + ), + terminal: false, + }) + lineReader.on('line', (line) => { + if (line.startsWith('#') || line.length == 0) { + return + } + expectInvalid(line) + }) + }) }) diff --git a/packages/syntax/tests/handle.test.ts b/packages/syntax/tests/handle.test.ts index 2fa56b4cfe2..d3eac90be2c 100644 --- a/packages/syntax/tests/handle.test.ts +++ b/packages/syntax/tests/handle.test.ts @@ -4,6 +4,8 @@ import { ensureValidHandleRegex, InvalidHandleError, } from '../src' +import * as readline from 'readline' +import * as fs from 'fs' describe('handle validation', () => { const expectValid = (h: string) => { @@ -190,6 +192,36 @@ describe('handle validation', () => { ] badStackoverflow.forEach(expectInvalid) }) + + it('conforms to interop valid handles', () => { + const lineReader = readline.createInterface({ + input: fs.createReadStream( + `${__dirname}/interop-files/handle_syntax_valid.txt`, + ), + terminal: false, + }) + lineReader.on('line', (line) => { + if (line.startsWith('#') || line.length == 0) { + return + } + expectValid(line) + }) + }) + + it('conforms to interop invalid handles', () => { + const lineReader = readline.createInterface({ + input: fs.createReadStream( + `${__dirname}/interop-files/handle_syntax_invalid.txt`, + ), + terminal: false, + }) + lineReader.on('line', (line) => { + if (line.startsWith('#') || line.length == 0) { + return + } + expectInvalid(line) + }) + }) }) describe('normalization', () => { diff --git a/packages/syntax/tests/interop-files/aturi_syntax_invalid.txt b/packages/syntax/tests/interop-files/aturi_syntax_invalid.txt new file mode 120000 index 00000000000..6e1cd7942fb --- /dev/null +++ b/packages/syntax/tests/interop-files/aturi_syntax_invalid.txt @@ -0,0 +1 @@ +../../../../interop-test-files/syntax/aturi_syntax_invalid.txt \ No newline at end of file diff --git a/packages/syntax/tests/interop-files/aturi_syntax_valid.txt b/packages/syntax/tests/interop-files/aturi_syntax_valid.txt new file mode 120000 index 00000000000..6dd011dcb6b --- /dev/null +++ b/packages/syntax/tests/interop-files/aturi_syntax_valid.txt @@ -0,0 +1 @@ +../../../../interop-test-files/syntax/aturi_syntax_valid.txt \ No newline at end of file diff --git a/packages/syntax/tests/interop-files/did_syntax_invalid.txt b/packages/syntax/tests/interop-files/did_syntax_invalid.txt new file mode 120000 index 00000000000..723fbf8091a --- /dev/null +++ b/packages/syntax/tests/interop-files/did_syntax_invalid.txt @@ -0,0 +1 @@ +../../../../interop-test-files/syntax/did_syntax_invalid.txt \ No newline at end of file diff --git a/packages/syntax/tests/interop-files/did_syntax_valid.txt b/packages/syntax/tests/interop-files/did_syntax_valid.txt new file mode 120000 index 00000000000..045a569d192 --- /dev/null +++ b/packages/syntax/tests/interop-files/did_syntax_valid.txt @@ -0,0 +1 @@ +../../../../interop-test-files/syntax/did_syntax_valid.txt \ No newline at end of file diff --git a/packages/syntax/tests/interop-files/handle_syntax_invalid.txt b/packages/syntax/tests/interop-files/handle_syntax_invalid.txt new file mode 120000 index 00000000000..1842714c308 --- /dev/null +++ b/packages/syntax/tests/interop-files/handle_syntax_invalid.txt @@ -0,0 +1 @@ +../../../../interop-test-files/syntax/handle_syntax_invalid.txt \ No newline at end of file diff --git a/packages/syntax/tests/interop-files/handle_syntax_valid.txt b/packages/syntax/tests/interop-files/handle_syntax_valid.txt new file mode 120000 index 00000000000..d8f700d05fb --- /dev/null +++ b/packages/syntax/tests/interop-files/handle_syntax_valid.txt @@ -0,0 +1 @@ +../../../../interop-test-files/syntax/handle_syntax_valid.txt \ No newline at end of file diff --git a/packages/syntax/tests/interop-files/nsid_syntax_invalid.txt b/packages/syntax/tests/interop-files/nsid_syntax_invalid.txt new file mode 120000 index 00000000000..62a0824ae1c --- /dev/null +++ b/packages/syntax/tests/interop-files/nsid_syntax_invalid.txt @@ -0,0 +1 @@ +../../../../interop-test-files/syntax/nsid_syntax_invalid.txt \ No newline at end of file diff --git a/packages/syntax/tests/interop-files/nsid_syntax_valid.txt b/packages/syntax/tests/interop-files/nsid_syntax_valid.txt new file mode 120000 index 00000000000..95a800d2d15 --- /dev/null +++ b/packages/syntax/tests/interop-files/nsid_syntax_valid.txt @@ -0,0 +1 @@ +../../../../interop-test-files/syntax/nsid_syntax_valid.txt \ No newline at end of file diff --git a/packages/syntax/tests/nsid.test.ts b/packages/syntax/tests/nsid.test.ts index c2d7f74ddf9..a57448ccabb 100644 --- a/packages/syntax/tests/nsid.test.ts +++ b/packages/syntax/tests/nsid.test.ts @@ -4,6 +4,8 @@ import { InvalidNsidError, NSID, } from '../src' +import * as readline from 'readline' +import * as fs from 'fs' describe('NSID parsing & creation', () => { it('parses valid NSIDs', () => { @@ -123,4 +125,34 @@ describe('NSID validation', () => { 'onion.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing', ) }) + + it('conforms to interop valid NSIDs', () => { + const lineReader = readline.createInterface({ + input: fs.createReadStream( + `${__dirname}/interop-files/nsid_syntax_valid.txt`, + ), + terminal: false, + }) + lineReader.on('line', (line) => { + if (line.startsWith('#') || line.length == 0) { + return + } + expectValid(line) + }) + }) + + it('conforms to interop invalid NSIDs', () => { + const lineReader = readline.createInterface({ + input: fs.createReadStream( + `${__dirname}/interop-files/nsid_syntax_invalid.txt`, + ), + terminal: false, + }) + lineReader.on('line', (line) => { + if (line.startsWith('#') || line.length == 0) { + return + } + expectInvalid(line) + }) + }) }) From 3877210e7fb3c76dfb1a11eb9ba3f18426301d9f Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 13 Sep 2023 15:56:12 -0500 Subject: [PATCH 237/237] add getSuggestedFollowsByActor (#1553) * add getSuggestedFollowsByActor lex * remove pagination * codegen * add pds route * add app view route * first pass at likes-based suggested actors, plus tests * format * backfill with suggested_follow table * combine actors queries * fall back to popular follows, handle backfill differently * revert seed change, update test * lower likes threshold * cleanup * remove todo * format * optimize queries * cover mute lists * clean up into pipeline steps * add changeset --- .changeset/seven-schools-switch.md | 6 + .../graph/getSuggestedFollowsByActor.json | 33 ++++ packages/api/src/client/index.ts | 18 +++ packages/api/src/client/lexicons.ts | 38 +++++ .../bsky/graph/getSuggestedFollowsByActor.ts | 36 +++++ .../bsky/graph/getSuggestedFollowsByActor.ts | 138 ++++++++++++++++ packages/bsky/src/api/index.ts | 2 + packages/bsky/src/lexicon/index.ts | 12 ++ packages/bsky/src/lexicon/lexicons.ts | 38 +++++ .../bsky/graph/getSuggestedFollowsByActor.ts | 46 ++++++ packages/bsky/tests/seeds/likes.ts | 29 ++++ .../tests/views/suggested-follows.test.ts | 147 ++++++++++++++++++ .../bsky/graph/getSuggestedFollowsByActor.ts | 20 +++ .../pds/src/app-view/api/app/bsky/index.ts | 2 + packages/pds/src/lexicon/index.ts | 12 ++ packages/pds/src/lexicon/lexicons.ts | 38 +++++ .../bsky/graph/getSuggestedFollowsByActor.ts | 46 ++++++ 17 files changed, 661 insertions(+) create mode 100644 .changeset/seven-schools-switch.md create mode 100644 lexicons/app/bsky/graph/getSuggestedFollowsByActor.json create mode 100644 packages/api/src/client/types/app/bsky/graph/getSuggestedFollowsByActor.ts create mode 100644 packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts create mode 100644 packages/bsky/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts create mode 100644 packages/bsky/tests/views/suggested-follows.test.ts create mode 100644 packages/pds/src/app-view/api/app/bsky/graph/getSuggestedFollowsByActor.ts create mode 100644 packages/pds/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts diff --git a/.changeset/seven-schools-switch.md b/.changeset/seven-schools-switch.md new file mode 100644 index 00000000000..012cf392426 --- /dev/null +++ b/.changeset/seven-schools-switch.md @@ -0,0 +1,6 @@ +--- +'@atproto/api': patch +--- + +Adds a new method `app.bsky.graph.getSuggestedFollowsByActor`. This method +returns suggested follows for a given actor based on their likes and follows. diff --git a/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json b/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json new file mode 100644 index 00000000000..32873a537c9 --- /dev/null +++ b/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json @@ -0,0 +1,33 @@ +{ + "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": "app.bsky.actor.defs#profileView" + } + } + } + } + } + } + } +} diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 16728348374..761097aad7c 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -113,6 +113,7 @@ 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 AppBskyGraphList from './types/app/bsky/graph/list' import * as AppBskyGraphListblock from './types/app/bsky/graph/listblock' import * as AppBskyGraphListitem from './types/app/bsky/graph/listitem' @@ -236,6 +237,7 @@ export * as AppBskyGraphGetListBlocks from './types/app/bsky/graph/getListBlocks export * as AppBskyGraphGetListMutes from './types/app/bsky/graph/getListMutes' export * as AppBskyGraphGetLists from './types/app/bsky/graph/getLists' export * as AppBskyGraphGetMutes from './types/app/bsky/graph/getMutes' +export * as AppBskyGraphGetSuggestedFollowsByActor from './types/app/bsky/graph/getSuggestedFollowsByActor' export * as AppBskyGraphList from './types/app/bsky/graph/list' export * as AppBskyGraphListblock from './types/app/bsky/graph/listblock' export * as AppBskyGraphListitem from './types/app/bsky/graph/listitem' @@ -1712,6 +1714,22 @@ export class GraphNS { }) } + getSuggestedFollowsByActor( + params?: AppBskyGraphGetSuggestedFollowsByActor.QueryParams, + opts?: AppBskyGraphGetSuggestedFollowsByActor.CallOptions, + ): Promise { + return this._service.xrpc + .call( + 'app.bsky.graph.getSuggestedFollowsByActor', + params, + undefined, + opts, + ) + .catch((e) => { + throw AppBskyGraphGetSuggestedFollowsByActor.toKnownErr(e) + }) + } + muteActor( data?: AppBskyGraphMuteActor.InputSchema, opts?: AppBskyGraphMuteActor.CallOptions, diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 30da3464cb2..f3c93c5e805 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -6109,6 +6109,42 @@ export const schemaDict = { }, }, }, + 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', @@ -6845,6 +6881,8 @@ export const ids = { 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', diff --git a/packages/api/src/client/types/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/api/src/client/types/app/bsky/graph/getSuggestedFollowsByActor.ts new file mode 100644 index 00000000000..8ff7ed414cb --- /dev/null +++ b/packages/api/src/client/types/app/bsky/graph/getSuggestedFollowsByActor.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 AppBskyActorDefs from '../actor/defs' + +export interface QueryParams { + actor: string +} + +export type InputSchema = undefined + +export interface OutputSchema { + suggestions: AppBskyActorDefs.ProfileView[] + [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/graph/getSuggestedFollowsByActor.ts b/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts new file mode 100644 index 00000000000..be42ce2b959 --- /dev/null +++ b/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -0,0 +1,138 @@ +import { sql } from 'kysely' +import { Server } from '../../../../lexicon' +import AppContext from '../../../../context' +import { InvalidRequestError } from '@atproto/xrpc-server' +import { Database } from '../../../../db' +import { ActorService } from '../../../../services/actor' + +const RESULT_LENGTH = 10 + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.graph.getSuggestedFollowsByActor({ + auth: ctx.authVerifier, + handler: async ({ auth, params }) => { + const { actor } = params + const viewer = auth.credentials.did + + const db = ctx.db.getReplica() + const actorService = ctx.services.actor(db) + const actorDid = await actorService.getActorDid(actor) + + if (!actorDid) { + throw new InvalidRequestError('Actor not found') + } + + const skeleton = await getSkeleton( + { + actor: actorDid, + viewer, + }, + { + db, + actorService, + }, + ) + const hydrationState = await actorService.views.profileDetailHydration( + skeleton.map((a) => a.did), + { viewer }, + ) + const presentationState = actorService.views.profileDetailPresentation( + skeleton.map((a) => a.did), + hydrationState, + { viewer }, + ) + const suggestions = Object.values(presentationState).filter((profile) => { + return ( + !profile.viewer?.muted && + !profile.viewer?.mutedByList && + !profile.viewer?.blocking && + !profile.viewer?.blockedBy + ) + }) + + return { + encoding: 'application/json', + body: { suggestions }, + } + }, + }) +} + +async function getSkeleton( + params: { + actor: string + viewer: string + }, + ctx: { + db: Database + actorService: ActorService + }, +): Promise<{ did: string }[]> { + const actorsViewerFollows = ctx.db.db + .selectFrom('follow') + .where('creator', '=', params.viewer) + .select('subjectDid') + const mostLikedAccounts = await ctx.db.db + .selectFrom( + ctx.db.db + .selectFrom('like') + .where('creator', '=', params.actor) + .select(sql`split_part(subject, '/', 3)`.as('subjectDid')) + .limit(1000) // limit to 1000 + .as('likes'), + ) + .select('likes.subjectDid as did') + .select((qb) => qb.fn.count('likes.subjectDid').as('count')) + .where('likes.subjectDid', 'not in', actorsViewerFollows) + .where('likes.subjectDid', 'not in', [params.actor, params.viewer]) + .groupBy('likes.subjectDid') + .orderBy('count', 'desc') + .limit(RESULT_LENGTH) + .execute() + const resultDids = mostLikedAccounts.map((a) => ({ did: a.did })) as { + did: string + }[] + + if (resultDids.length < RESULT_LENGTH) { + // backfill with popular accounts followed by actor + const mostPopularAccountsActorFollows = await ctx.db.db + .selectFrom('follow') + .innerJoin('profile_agg', 'follow.subjectDid', 'profile_agg.did') + .select('follow.subjectDid as did') + .where('follow.creator', '=', params.actor) + .where('follow.subjectDid', '!=', params.viewer) + .where('follow.subjectDid', 'not in', actorsViewerFollows) + .if(resultDids.length > 0, (qb) => + qb.where( + 'subjectDid', + 'not in', + resultDids.map((a) => a.did), + ), + ) + .orderBy('profile_agg.followersCount', 'desc') + .limit(RESULT_LENGTH) + .execute() + + resultDids.push(...mostPopularAccountsActorFollows) + } + + if (resultDids.length < RESULT_LENGTH) { + // backfill with suggested_follow table + const additional = await ctx.db.db + .selectFrom('suggested_follow') + .where( + 'did', + 'not in', + // exclude any we already have + resultDids.map((a) => a.did).concat([params.actor, params.viewer]), + ) + // and aren't already followed by viewer + .where('did', 'not in', actorsViewerFollows) + .selectAll() + .execute() + + resultDids.push(...additional) + } + + return resultDids +} diff --git a/packages/bsky/src/api/index.ts b/packages/bsky/src/api/index.ts index ec64c2236bf..1928cda01d2 100644 --- a/packages/bsky/src/api/index.ts +++ b/packages/bsky/src/api/index.ts @@ -28,6 +28,7 @@ import muteActor from './app/bsky/graph/muteActor' import unmuteActor from './app/bsky/graph/unmuteActor' import muteActorList from './app/bsky/graph/muteActorList' import unmuteActorList from './app/bsky/graph/unmuteActorList' +import getSuggestedFollowsByActor from './app/bsky/graph/getSuggestedFollowsByActor' import searchActors from './app/bsky/actor/searchActors' import searchActorsTypeahead from './app/bsky/actor/searchActorsTypeahead' import getSuggestions from './app/bsky/actor/getSuggestions' @@ -87,6 +88,7 @@ export default function (server: Server, ctx: AppContext) { unmuteActor(server, ctx) muteActorList(server, ctx) unmuteActorList(server, ctx) + getSuggestedFollowsByActor(server, ctx) searchActors(server, ctx) searchActorsTypeahead(server, ctx) getSuggestions(server, ctx) diff --git a/packages/bsky/src/lexicon/index.ts b/packages/bsky/src/lexicon/index.ts index 028b3cbf397..93435056503 100644 --- a/packages/bsky/src/lexicon/index.ts +++ b/packages/bsky/src/lexicon/index.ts @@ -96,6 +96,7 @@ 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' @@ -1251,6 +1252,17 @@ export class GraphNS { 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, diff --git a/packages/bsky/src/lexicon/lexicons.ts b/packages/bsky/src/lexicon/lexicons.ts index 30da3464cb2..f3c93c5e805 100644 --- a/packages/bsky/src/lexicon/lexicons.ts +++ b/packages/bsky/src/lexicon/lexicons.ts @@ -6109,6 +6109,42 @@ export const schemaDict = { }, }, }, + 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', @@ -6845,6 +6881,8 @@ export const ids = { 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', diff --git a/packages/bsky/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/bsky/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts new file mode 100644 index 00000000000..a2245846fd2 --- /dev/null +++ b/packages/bsky/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/bsky/tests/seeds/likes.ts b/packages/bsky/tests/seeds/likes.ts index 27eeba09c40..1747fb2fa59 100644 --- a/packages/bsky/tests/seeds/likes.ts +++ b/packages/bsky/tests/seeds/likes.ts @@ -10,5 +10,34 @@ export default async (sc: SeedClient) => { }) await sc.like(sc.dids.eve, sc.posts[sc.dids.alice][1].ref) await sc.like(sc.dids.carol, sc.replies[sc.dids.bob][0].ref) + + // give alice > 100 likes + for (let i = 0; i < 50; i++) { + const [b, c, d] = await Promise.all([ + sc.post(sc.dids.bob, `bob post ${i}`), + sc.post(sc.dids.carol, `carol post ${i}`), + sc.post(sc.dids.dan, `dan post ${i}`), + ]) + await Promise.all( + [ + sc.like(sc.dids.alice, b.ref), // likes 50 of bobs posts + i < 45 && sc.like(sc.dids.alice, c.ref), // likes 45 of carols posts + i < 40 && sc.like(sc.dids.alice, d.ref), // likes 40 of dans posts + ].filter(Boolean), + ) + } + + // couple more NPCs for suggested follows + await sc.createAccount('fred', { + email: 'fred@test.com', + handle: 'fred.test', + password: 'fred-pass', + }) + await sc.createAccount('gina', { + email: 'gina@test.com', + handle: 'gina.test', + password: 'gina-pass', + }) + return sc } diff --git a/packages/bsky/tests/views/suggested-follows.test.ts b/packages/bsky/tests/views/suggested-follows.test.ts new file mode 100644 index 00000000000..6a2f3ebe1d7 --- /dev/null +++ b/packages/bsky/tests/views/suggested-follows.test.ts @@ -0,0 +1,147 @@ +import AtpAgent, { AtUri } from '@atproto/api' +import { TestNetwork } from '@atproto/dev-env' +import { SeedClient } from '../seeds/client' +import likesSeed from '../seeds/likes' + +describe('suggested follows', () => { + let network: TestNetwork + let agent: AtpAgent + let pdsAgent: AtpAgent + let sc: SeedClient + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_views_suggestions', + }) + agent = network.bsky.getClient() + pdsAgent = network.pds.getClient() + sc = new SeedClient(pdsAgent) + await likesSeed(sc) + await network.processAll() + await network.bsky.processAll() + + const suggestions = [ + { did: sc.dids.alice, order: 1 }, + { did: sc.dids.bob, order: 2 }, + { did: sc.dids.carol, order: 3 }, + { did: sc.dids.dan, order: 4 }, + { did: sc.dids.fred, order: 5 }, + { did: sc.dids.gina, order: 6 }, + ] + await network.bsky.ctx.db + .getPrimary() + .db.insertInto('suggested_follow') + .values(suggestions) + .execute() + }) + + afterAll(async () => { + await network.close() + }) + + it('returns sorted suggested follows for carol', async () => { + const result = await agent.api.app.bsky.graph.getSuggestedFollowsByActor( + { + actor: sc.dids.alice, + }, + { headers: await network.serviceHeaders(sc.dids.carol) }, + ) + + expect(result.data.suggestions.length).toBe(4) // backfilled with 2 NPCs + expect( + result.data.suggestions.find((sug) => { + return [sc.dids.alice, sc.dids.carol].includes(sug.did) + }), + ).toBeFalsy() // not actor or viewer + }) + + it('returns sorted suggested follows for fred', async () => { + const result = await agent.api.app.bsky.graph.getSuggestedFollowsByActor( + { + actor: sc.dids.alice, + }, + { headers: await network.serviceHeaders(sc.dids.fred) }, + ) + + expect(result.data.suggestions.length).toBe(4) // backfilled with 2 NPCs + expect( + result.data.suggestions.find((sug) => { + return [sc.dids.fred, sc.dids.alice].includes(sug.did) + }), + ).toBeFalsy() // not actor or viewer or followed + }) + + it('exludes users muted by viewer', async () => { + await pdsAgent.api.app.bsky.graph.muteActor( + { actor: sc.dids.bob }, + { headers: sc.getHeaders(sc.dids.carol), encoding: 'application/json' }, + ) + const result = await agent.api.app.bsky.graph.getSuggestedFollowsByActor( + { + actor: sc.dids.alice, + }, + { headers: await network.serviceHeaders(sc.dids.carol) }, + ) + + expect( + result.data.suggestions.find((sug) => { + return [sc.dids.alice, sc.dids.carol, sc.dids.bob].includes(sug.did) + }), + ).toBeFalsy() // not actor or viewer or muted + + await pdsAgent.api.app.bsky.graph.muteActor( + { actor: sc.dids.bob }, + { headers: sc.getHeaders(sc.dids.carol), encoding: 'application/json' }, + ) + }) + + it('exludes users blocked by viewer', async () => { + const carolBlocksBob = await pdsAgent.api.app.bsky.graph.block.create( + { repo: sc.dids.carol }, + { createdAt: new Date().toISOString(), subject: sc.dids.bob }, + sc.getHeaders(sc.dids.carol), + ) + const result = await agent.api.app.bsky.graph.getSuggestedFollowsByActor( + { + actor: sc.dids.alice, + }, + { headers: await network.serviceHeaders(sc.dids.carol) }, + ) + + expect( + result.data.suggestions.find((sug) => { + return [sc.dids.alice, sc.dids.carol, sc.dids.bob].includes(sug.did) + }), + ).toBeFalsy() // not actor or viewer or muted + + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: sc.dids.carol, rkey: new AtUri(carolBlocksBob.uri).rkey }, + sc.getHeaders(sc.dids.carol), + ) + }) + + it('exludes users blocking viewer', async () => { + const bobBlocksCarol = await pdsAgent.api.app.bsky.graph.block.create( + { repo: sc.dids.bob }, + { createdAt: new Date().toISOString(), subject: sc.dids.carol }, + sc.getHeaders(sc.dids.bob), + ) + const result = await agent.api.app.bsky.graph.getSuggestedFollowsByActor( + { + actor: sc.dids.alice, + }, + { headers: await network.serviceHeaders(sc.dids.carol) }, + ) + + expect( + result.data.suggestions.find((sug) => { + return [sc.dids.alice, sc.dids.carol, sc.dids.bob].includes(sug.did) + }), + ).toBeFalsy() // not actor or viewer or muted + + await pdsAgent.api.app.bsky.graph.block.delete( + { repo: sc.dids.bob, rkey: new AtUri(bobBlocksCarol.uri).rkey }, + sc.getHeaders(sc.dids.bob), + ) + }) +}) diff --git a/packages/pds/src/app-view/api/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/pds/src/app-view/api/app/bsky/graph/getSuggestedFollowsByActor.ts new file mode 100644 index 00000000000..dfafa6b65ea --- /dev/null +++ b/packages/pds/src/app-view/api/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -0,0 +1,20 @@ +import { Server } from '../../../../../lexicon' +import AppContext from '../../../../../context' + +export default function (server: Server, ctx: AppContext) { + server.app.bsky.graph.getSuggestedFollowsByActor({ + auth: ctx.accessVerifier, + handler: async ({ auth, params }) => { + const requester = auth.credentials.did + const res = + await ctx.appviewAgent.api.app.bsky.graph.getSuggestedFollowsByActor( + params, + await ctx.serviceAuthHeaders(requester), + ) + return { + encoding: 'application/json', + body: res.data, + } + }, + }) +} diff --git a/packages/pds/src/app-view/api/app/bsky/index.ts b/packages/pds/src/app-view/api/app/bsky/index.ts index 7f9c458ae70..5cffb90653d 100644 --- a/packages/pds/src/app-view/api/app/bsky/index.ts +++ b/packages/pds/src/app-view/api/app/bsky/index.ts @@ -27,6 +27,7 @@ import muteActor from './graph/muteActor' import muteActorList from './graph/muteActorList' import unmuteActor from './graph/unmuteActor' import unmuteActorList from './graph/unmuteActorList' +import getSuggestedFollowsByActor from './graph/getSuggestedFollowsByActor' import getUsersSearch from './actor/searchActors' import getUsersTypeahead from './actor/searchActorsTypeahead' import getSuggestions from './actor/getSuggestions' @@ -64,6 +65,7 @@ export default function (server: Server, ctx: AppContext) { muteActorList(server, ctx) unmuteActor(server, ctx) unmuteActorList(server, ctx) + getSuggestedFollowsByActor(server, ctx) getUsersSearch(server, ctx) getUsersTypeahead(server, ctx) getSuggestions(server, ctx) diff --git a/packages/pds/src/lexicon/index.ts b/packages/pds/src/lexicon/index.ts index 028b3cbf397..93435056503 100644 --- a/packages/pds/src/lexicon/index.ts +++ b/packages/pds/src/lexicon/index.ts @@ -96,6 +96,7 @@ 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' @@ -1251,6 +1252,17 @@ export class GraphNS { 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, diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 30da3464cb2..f3c93c5e805 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -6109,6 +6109,42 @@ export const schemaDict = { }, }, }, + 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', @@ -6845,6 +6881,8 @@ export const ids = { 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', diff --git a/packages/pds/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/pds/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts new file mode 100644 index 00000000000..a2245846fd2 --- /dev/null +++ b/packages/pds/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